npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@lcsga/ng-operators

v5.0.0

Published

Provides custom RxJS operators to empower the use of Angular with RxJS

Downloads

11

Readme

@lcsga/ng-operators

Breaking changes:

v5.0.0

  • The fromChildEvent, fromChildrenEvent, fromChildOuptut and fromChildrenOutput buildNotifier options are now replaced with injector
  • The fromHostEvent host option is now replaced with injector

v4.0.0

  • fromChildrenEvent now returns an Observable<readonly [event: Event, index: number]>

v3.0.0

  • update angular to v16 (will throw a nice error message if it's called without access to inject)

v2.0.0

  • fromChildEvent and fromChildrenEvent must now be used in an injection context (or you can now provide a new buildNotifier option if needed).

This package provides a set of custom RxJS operators, used to make declarative pattern easier to set up within an angular app.

Getting started

Installation

@lcsga/ng-operators is available for download at npm.

npm install @lcsga/ng-operators

Configuration

To work as expected, this library needs the NgZone configuration to enable the event coalescing:

export const appConfig: ApplicationConfig = {
  providers: [
    // ...
    provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }),
  ],
};
// You can also activate the runCoalescing option alongside with the OnPush detection strategy to provide better performances of your apps

Documentation

Table des matières

fromChildEvent

This operator is usefull whenever you want to listen to some @ViewChild or @ContentChild events.

fromChildEvent<T extends Event>(
  childSelector: () => ElementRef | undefined,
  type: keyof HTMLElementEventMap,
  options?: NgEventListenerOptions
): Observable<T>

| argument | type | description | | --------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | childSelector | () => ElementRef \| undefined | A callback function used to get the child element to listen an event to. | | type | keyof HTMLElementEventMap | The type of event to listen. | | options | NgEventListenerOptions | Optional. Default is {}.Options to pass through to the underlying addEventListener function or if you need to manualy provide the injector |

Currently, when you want to avoid using the Angular's @HostListner or the (click)="doSomething()", you can create a Subject and use it like this:

@Component({
    selector: "app",
    template: "<button (click)="buttonClick$$.next()">Click me!</button>"
})
export class AppComponent {
    protected readonly buttonClick$$ = new Subject<void>();

    private readonly onButtonClick$ = this.buttonClick$$.pipe(
        tap(() => console.log("hello world!"))
    );

    constructor() {
        this.onButtonClick$.subscribe();
    }
}

It actually works pretty well but since we need to specifically call the next() method of the buttonClick$$ subject, it's not fully declarative.

To make it declarative, we would instead need to use the fromEvent operator from RxJS but we can't do that nicely because it takes an element from the dom.

Indeed to get such an element in Angular, depending on what you've built, you can either use @ViewChild or @ContentChild

@Component({
  selector: 'app',
  template: '<button #button>Click me!</button>',
})
export class AppComponent {
  @ViewChild('button')
  private readonly button?: ElementRef<HTMLButtonElement>;

  private readonly onButtonClick$ = fromEvent(this.button?.nativeElement, 'click').pipe(
    // throws an error!
    tap(() => console.log('hello world!'))
  );

  constructor() {
    this.onButtonClick$.subscribe();
  }
}

The issue with the code above is that the button element is undefined until the dom is rendered. Thus the Cannot read properties of undefined (reading 'addEventListener') error is thrown.

A solution to make it work would be to assign the stream of onButtonClick$ within the afterViewInit() method but the best part of declarative is to write the assigning right at the declaration, so it wouldn't be prefect.

Here comes the fromChildEvent custom operator, to the rescue! It works by listening the event of your choice directly on the document and check if the event's target is the same as a viewChild or a contentChild you'd pass to it

Example:
@Component({
  selector: 'app',
  template: '<button #button>Click me!</button>',
})
export class AppComponent {
  @ViewChild('button')
  private readonly button?: ElementRef<HTMLButtonElement>;

  private readonly onButtonClick$ = fromChildEvent(() => this.button, 'click').pipe(tap(() => console.log('hello world!')));

  constructor() {
    this.onButtonClick$.subscribe();
  }
}

As you can see, fromChildEvent takes a selector callback to get the viewChild or contentChild target.

Since the document's event can only be fired after the dom is rendered, we know that the element passed within the selector callback is always available.

fromChildrenEvent

It works exactly like fromChildEvent but with @ViewChildren or @ContentChildren instead!

  fromChildrenEvent<T extends Event>(
    childrenSelector: () => ElementRef[] | undefined,
    type: keyof HTMLElementEventMap,
    options?: NgEventListenerOptions
  ): Observable<readonly [event: T, index: number]>

| argument | type | description | | ------------------ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | childrenSelector | () => ElementRef[] \| undefined | A callback function used to get the children elements to listen an event to. | | type | keyof HTMLElementEventMap | The type of event to listen. | | options | NgEventListenerOptions | Optional. Default is {}.Options to pass through to the underlying addEventListener function or if you need to manualy provide the injector |

<br/>
Example:
@Component({
  selector: 'app',
  template: `
    <button #button>Click me!</button>

    <button #button>Click me!</button>

    <button #button>Click me!</button>
  `,
})
export class AppComponent {
  @ViewChildren('button')
  private readonly buttons?: QueryList<ElementRef<HTMLButtonElement>>;

  private readonly onButtonsClick$ = fromChildrenEvent(() => this.buttons?.toArray(), 'click').pipe(tap(() => console.log('hello world!')));

  constructor() {
    this.onButtonsClick$.subscribe();
  }
}

fromHostEvent

This operator is usefull as an Rx replacement for @HostListner.

  fromHostEvent<T extends Event>(type: keyof HTMLElementEventMap, options?: NgEventListenerOptions): Observable<T>

| argument | type | description | | --------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | type | keyof HTMLElementEventMap | The type of event to listen. | | options | NgEventListenerOptions | Optional. Default is {}.Options to pass through to the underlying addEventListener function or if you need to manualy provide the injector |

Example:
@Component({
  selector: 'app-root',
  template: '',
})
export class AppComponent {
  constructor() {
    fromHostEvent('click').subscribe(() => console.log('hello world!'));
    // on app-root click => Output: hello world!
  }
}

fromChildOutput

This operator is usefull whenever you want to listen to some @ViewChild or @ContentChild outputs or observables for a Component or a Directive instead of an ElementRef.

  fromChildOutput<TChild extends object, TOutput extends PickOutput<TChild>, TOutputName extends keyof TOutput>(
    childSelector: () => TChild | undefined,
    outputName: TOutputName,
    options?: InjectorOption
  ): Observable<TOutput[TOutputName]>

See PickOutput utility type

| argument | type | description | | --------------- | --------------------------- | ---------------------------------------------------------------------------------------------------- | | childSelector | () => TChild \| undefined | A callback function used to get the child element to listen an event to. | | outputName | TOutputName | The name of the public observable to listen. | | options | InjectorOption | Optional. Default is {}.Options to pass through if you need to manualy provide the injector |

<br/>
Example:
@Component({
  selector: 'app-child',
  template: `<button (click)="sayHello.emit('hello!')">Say hello</button>`,
})
class AppChildComponent {
  // @Output is not necessary here, if you don't use the angular (eventName)="doSomething()" syntax
  // 'sayHello' must be public to be accessed
  sayHello = new EventEmitter<string>();
}

@Component({
  selector: 'app-root',
  imports: [AppChildComponent],
  template: '<app-child #child />',
})
export class AppComponent {
  @ViewChild('child') child!: AppChildComponent;

  constructor() {
    // the second argument is infered from the child as the childSelector
    fromChildOutput(() => this.child, 'sayHello').subscribe(console.log);
    // on child button click => Output: hello!
  }
}

fromChildrenOutput

It works exactly like fromChildOutput but for an array of components instead!

  fromChildrenOutput<
    TChildren extends object[],
    TUnionOutput extends PickUnionOutput<TChildren>,
    TUnionOutputName extends keyof TUnionOutput
  >(
    childrenSelector: () => TChildren | undefined,
    outputName: TUnionOutputName,
    options?: InjectorOption
  ): Observable<readonly [output: TUnionOutput[TUnionOutputName], index: number]>

See PickOutput utility type

| argument | type | description | | ------------------ | ------------------------------ | ---------------------------------------------------------------------------------------------------- | | childrenSelector | () => TChildren \| undefined | A callback function used to get the child element to listen an event to. | | outputName | TOutputName | The name of the public observable to listen. | | options | InjectorOption | Optional. Default is {}.Options to pass through if you need to manualy provide the injector |

<br/>
Example:
@Component({
  selector: 'app-child',
  template: `<button (click)="say.emit('hello!')">Say hello</button>`,
})
class AppChildComponent {
  // @Output is not necessary here, if you don't use the angular (eventName)="doSomething()" syntax
  // 'sayHello' must be public to be accessed
  say = new EventEmitter<string>();
}

@Component({
  selector: 'app-child2',
  template: `<button (click)="say.emit('goodbye!')">Say goodbye</button>`,
})
class AppChild2Component {
  say = new EventEmitter<string>();

  say$ = this.say.asObservablee(); // This won't be available as the second arguement `outputName` of `fromChildrenOutput` since it does not exist on `AppChildComponent`
}

@Component({
  selector: 'app-root',
  imports: [AppChildComponent, AppChild2Component],
  template: `
    <app-child #child />

    <app-child2 #child2 />
  `,
})
export class AppComponent {
  @ViewChild('child') child!: AppChildComponent;
  // Since it takes an array of components, you can rebuild it in your own way
  @ViewChild('child2') child2!: AppChild2Component;

  constructor() {
    // The second argument is infered from the child as the childSelector
    fromChildrenOutput(() => [this.child, this.child2], 'say').subscribe(console.log);
    // On child button click => Output: ["hello!", 0]
  }
}

rxAfterNextRender

It uses the new afterNextRender to send an RxJS notification that the callback function has been called once.

  rxAfterNextRender(injector?: Injector): Observable<void>

| argument | type | description | | ---------- | ---------- | --------------------------------- | | injector | Injector | Optional. Default is undefined. |

Example
@Component({
  selector: 'app',
  template: '<button #button>Click me!</button>',
})
export class AppComponent {
  @ViewChild('button')
  private readonly button?: ElementRef<HTMLButtonElement>;

  constructor() {
    rxAfterNextRender().subscribe(() => console.log(this.button?.clientHeight)); // the button won't be undefined here
  }
}

rxAfterRender

It uses the new afterRender to send RxJS notifications each time the callback function is called.

  rxAfterRender(injector?: Injector): Observable<void>

| argument | type | description | | ---------- | ---------- | --------------------------------- | | injector | Injector | Optional. Default is undefined. |

Example
@Component({
  selector: 'app',
  template: '<button #button>Click me!</button>',
})
export class AppComponent {
  @ViewChild('button')
  private readonly button?: ElementRef<HTMLButtonElement>;

  constructor() {
    rxAfterRender().subscribe(() => console.log(this.button?.clientHeight)); // Will log the button's clientHeight each time the view is checked by angular
  }
}

What to expect in the future?

With the upcomming Signal-based Components, we shouldn't need to first declare @ViewChild, @ViewChildren, etc. anymore.-ml-1

This would greatly improve the DX of these operators and it could lead to the following improvements:

@Component({
  selector: 'app',
  template: '<button #button>Click me!</button>',
})
export class AppComponent {
  readonly onButtonClick$ = fromViewChildEvent('button', 'click').pipe(...)
}
@Component({
  selector: 'app',
  template: '<button mat-button>Click me!</button>',
})
export class AppComponent {
  readonly onButtonClick$ = fromViewChildEvent(MatButton, 'click').pipe(...)
}
@Component({
  selector: 'app',
  template: `
    <button #button1>Click me!</button>

    <button #button2>Click me too!</button>
  `,
})
export class AppComponent {
  readonly onButtonsClick$ = fromViewChildrenEvent(['button1', 'button2'], 'click').pipe(...)
  // of course, the 2 buttons could simply be named #button but it is for the example only
}
@Component({
  selector: 'app',
  template: `
    <button mat-button>Click me!</button>

    <button mat-button>Click me too!</button>
  `,
})
export class AppComponent {
  readonly onButtonClick$ = fromViewChildrenEvent(MatButton, 'click').pipe(...)
  // or could be fromViewChildren([MatButton, SomeOtherComponentOrDirective]).pipe(...);
}

| With SBCs, I could either decouple the viewChild<ren> and contentChild<ren> or try to merge them together.

=> Another thing I might improve is merging fromChild<ren>Event and fromChild<ren>Output into one operator.