UNPKG

@ngrx/component

Version:

Reactive Extensions for Angular Components

496 lines (484 loc) 18.3 kB
import * as i0 from '@angular/core'; import { NgZone, inject, Injectable, ApplicationRef, PLATFORM_ID, ChangeDetectorRef, untracked, Input, Directive, Pipe } from '@angular/core'; import { isPlatformServer } from '@angular/common'; import { isObservable, combineLatest, from, Observable, ReplaySubject, pipe, Subscription } from 'rxjs'; import { distinctUntilChanged, tap, switchMap } from 'rxjs/operators'; function isNgZone(zone) { return zone instanceof NgZone; } class TickScheduler { /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TickScheduler, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TickScheduler, providedIn: 'root', useFactory: () => { const zone = inject(NgZone); return isNgZone(zone) ? new NoopTickScheduler() : inject(ZonelessTickScheduler); } }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: TickScheduler, decorators: [{ type: Injectable, args: [{ providedIn: 'root', useFactory: () => { const zone = inject(NgZone); return isNgZone(zone) ? new NoopTickScheduler() : inject(ZonelessTickScheduler); }, }] }] }); class ZonelessTickScheduler extends TickScheduler { constructor() { super(...arguments); this.appRef = inject(ApplicationRef); this.platformId = inject(PLATFORM_ID); this.isServer = isPlatformServer(this.platformId); this.scheduleFn = this.isServer ? setTimeout : requestAnimationFrame; this.isScheduled = false; } schedule() { if (!this.isScheduled) { this.isScheduled = true; this.scheduleFn(() => { this.appRef.tick(); this.isScheduled = false; }); } } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ZonelessTickScheduler, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); } /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ZonelessTickScheduler, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ZonelessTickScheduler, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); class NoopTickScheduler extends TickScheduler { // eslint-disable-next-line @typescript-eslint/no-empty-function schedule() { } } /** * Provides rendering functionality regardless of whether `zone.js` is present * or not. It must be provided at the component/directive level. * * @usageNotes * * ### Rerender zone-less app on route changes * * ```ts * @Component({ * selector: 'app-root', * template: '<router-outlet>', * // 👇 `RenderScheduler` is provided at the component level * providers: [RenderScheduler], * changeDetection: ChangeDetectionStrategy.OnPush, * }) * export class AppComponent implements OnInit { * constructor( * private readonly router: Router, * private readonly renderScheduler: RenderScheduler * ) {} * * ngOnInit(): void { * this.router.events * .pipe(filter((e) => e instanceof NavigationEnd)) * .subscribe(() => this.renderScheduler.schedule()); * } * } * ``` * * ### Rerender component on interval * * ```ts * @Component({ * selector: 'app-interval', * template: '{{ elapsedTime }}ms', * // 👇 `RenderScheduler` is provided at the component level * providers: [RenderScheduler], * changeDetection: ChangeDetectionStrategy.OnPush, * }) * export class IntervalComponent implements OnInit { * elapsedTime = 0; * * constructor(private readonly renderScheduler: RenderScheduler) {} * * ngOnInit(): void { * setInterval(() => { * this.elapsedTime += 1000; * this.renderScheduler.schedule(); * }, 1000); * } * } * ``` */ class RenderScheduler { constructor(cdRef, tickScheduler) { this.cdRef = cdRef; this.tickScheduler = tickScheduler; } /** * Marks component and its ancestors as dirty. * It also schedules a new change detection cycle in zone-less mode. */ schedule() { this.cdRef.markForCheck(); this.tickScheduler.schedule(); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: RenderScheduler, deps: [{ token: i0.ChangeDetectorRef }, { token: TickScheduler }], target: i0.ɵɵFactoryTarget.Injectable }); } /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: RenderScheduler }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: RenderScheduler, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }, { type: TickScheduler }] }); function createRenderScheduler() { return new RenderScheduler(inject(ChangeDetectorRef), inject(TickScheduler)); } function combineRenderEventHandlers(handlers) { return (event) => handlers[event.type]?.(event); } function fromPotentialObservable(potentialObservable) { if (isObservable(potentialObservable)) { return potentialObservable; } if (isObservableDictionary(potentialObservable)) { return combineLatest(toDistinctObsDictionary(potentialObservable)); } if (isPromiseLike(potentialObservable)) { return from(potentialObservable); } return new Observable((subscriber) => { subscriber.next(potentialObservable); }); } function isPromiseLike(value) { return typeof value?.then === 'function'; } function isObservableDictionary(value) { return (isDictionary(value) && Object.keys(value).length > 0 && Object.values(value).every(isObservable)); } function isDictionary(value) { return !!value && typeof value === 'object' && !Array.isArray(value); } function toDistinctObsDictionary(obsDictionary) { return Object.keys(obsDictionary).reduce((acc, key) => ({ ...acc, [key]: obsDictionary[key].pipe(distinctUntilChanged()), }), {}); } function createRenderEventManager(handlers) { const handleRenderEvent = combineRenderEventHandlers(handlers); const potentialObservable$ = new ReplaySubject(1); return { nextPotentialObservable(potentialObservable) { potentialObservable$.next(potentialObservable); }, handlePotentialObservableChanges() { return potentialObservable$.pipe(distinctUntilChanged(), switchMapToRenderEvent(), distinctUntilChanged(renderEventComparator), tap(handleRenderEvent)); }, }; } function switchMapToRenderEvent() { return pipe(switchMap((potentialObservable) => { const observable$ = fromPotentialObservable(potentialObservable); let reset = true; let synchronous = true; return new Observable((subscriber) => { const subscription = untracked(() => observable$.subscribe({ next(value) { subscriber.next({ type: 'next', value, reset, synchronous }); reset = false; }, error(error) { subscriber.next({ type: 'error', error, reset, synchronous }); reset = false; }, complete() { subscriber.next({ type: 'complete', reset, synchronous }); reset = false; }, })); if (reset) { subscriber.next({ type: 'suspense', reset, synchronous: true }); reset = false; } synchronous = false; return subscription; }); })); } function renderEventComparator(previous, current) { if (previous.type !== current.type || previous.reset !== current.reset) { return false; } if (current.type === 'next') { return previous.value === current.value; } if (current.type === 'error') { return previous.error === current.error; } return true; } /** * * @description * * The `*ngrxLet` directive serves a convenient way of binding observables to a view context * (DOM element's scope). It also helps with several internal processing under the hood. * * @usageNotes * * ### Displaying Observable Values * * ```html * <ng-container *ngrxLet="number$ as n"> * <app-number [number]="n"></app-number> * </ng-container> * * <ng-container *ngrxLet="number$; let n"> * <app-number [number]="n"></app-number> * </ng-container> * ``` * * ### Tracking Different Observable Events * * ```html * <ng-container *ngrxLet="number$ as n; error as e; complete as c"> * <app-number [number]="n" *ngIf="!e && !c"> * </app-number> * * <p *ngIf="e">There is an error: {{ e }}</p> * <p *ngIf="c">Observable is completed.</p> * </ng-container> * ``` * * ### Combining Multiple Observables * * ```html * <ng-container *ngrxLet="{ users: users$, query: query$ } as vm"> * <app-search-bar [query]="vm.query"></app-search-bar> * <app-user-list [users]="vm.users"></app-user-list> * </ng-container> * ``` * * ### Using Suspense Template * * ```html * <ng-container *ngrxLet="number$ as n; suspenseTpl: loading"> * <app-number [number]="n"></app-number> * </ng-container> * * <ng-template #loading> * <p>Loading...</p> * </ng-template> * ``` * * ### Using Aliases for Non-Observable Values * * ```html * <ng-container *ngrxLet="userForm.controls.email as email"> * <input type="text" [formControl]="email" /> * * <ng-container *ngIf="email.errors && (email.touched || email.dirty)"> * <p *ngIf="email.errors.required">This field is required.</p> * <p *ngIf="email.errors.email">This field must be an email.</p> * </ng-container> * </ng-container> * ``` * * @publicApi */ class LetDirective { set ngrxLet(potentialObservable) { this.renderEventManager.nextPotentialObservable(potentialObservable); } constructor(mainTemplateRef, viewContainerRef, errorHandler, renderScheduler) { this.mainTemplateRef = mainTemplateRef; this.viewContainerRef = viewContainerRef; this.errorHandler = errorHandler; this.renderScheduler = renderScheduler; this.isMainViewCreated = false; this.isSuspenseViewCreated = false; this.viewContext = { $implicit: undefined, ngrxLet: undefined, error: undefined, complete: false, }; this.renderEventManager = createRenderEventManager({ suspense: () => { this.viewContext.$implicit = undefined; this.viewContext.ngrxLet = undefined; this.viewContext.error = undefined; this.viewContext.complete = false; this.renderSuspenseView(); }, next: (event) => { this.viewContext.$implicit = event.value; this.viewContext.ngrxLet = event.value; if (event.reset) { this.viewContext.error = undefined; this.viewContext.complete = false; } this.renderMainView(event.synchronous); }, error: (event) => { this.viewContext.error = event.error; if (event.reset) { this.viewContext.$implicit = undefined; this.viewContext.ngrxLet = undefined; this.viewContext.complete = false; } this.renderMainView(event.synchronous); this.errorHandler.handleError(event.error); }, complete: (event) => { this.viewContext.complete = true; if (event.reset) { this.viewContext.$implicit = undefined; this.viewContext.ngrxLet = undefined; this.viewContext.error = undefined; } this.renderMainView(event.synchronous); }, }); this.subscription = new Subscription(); } static ngTemplateContextGuard(dir, ctx) { return true; } ngOnInit() { this.subscription.add(this.renderEventManager.handlePotentialObservableChanges().subscribe()); } ngOnDestroy() { this.subscription.unsubscribe(); } renderMainView(isSyncEvent) { if (this.isSuspenseViewCreated) { this.isSuspenseViewCreated = false; this.viewContainerRef.clear(); } if (!this.isMainViewCreated) { this.isMainViewCreated = true; this.viewContainerRef.createEmbeddedView(this.mainTemplateRef, this.viewContext); } if (!isSyncEvent) { this.renderScheduler.schedule(); } } renderSuspenseView() { if (this.isMainViewCreated) { this.isMainViewCreated = false; this.viewContainerRef.clear(); } if (this.suspenseTemplateRef && !this.isSuspenseViewCreated) { this.isSuspenseViewCreated = true; this.viewContainerRef.createEmbeddedView(this.suspenseTemplateRef); } } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: LetDirective, deps: [{ token: i0.TemplateRef }, { token: i0.ViewContainerRef }, { token: i0.ErrorHandler }, { token: RenderScheduler }], target: i0.ɵɵFactoryTarget.Directive }); } /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.0", type: LetDirective, isStandalone: true, selector: "[ngrxLet]", inputs: { ngrxLet: "ngrxLet", suspenseTemplateRef: ["ngrxLetSuspenseTpl", "suspenseTemplateRef"] }, providers: [RenderScheduler], ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: LetDirective, decorators: [{ type: Directive, args: [{ selector: '[ngrxLet]', providers: [RenderScheduler], }] }], ctorParameters: () => [{ type: i0.TemplateRef }, { type: i0.ViewContainerRef }, { type: i0.ErrorHandler }, { type: RenderScheduler }], propDecorators: { ngrxLet: [{ type: Input }], suspenseTemplateRef: [{ type: Input, args: ['ngrxLetSuspenseTpl'] }] } }); /** * @description * * The `ngrxPush` pipe serves as a drop-in replacement for the `async` pipe. * It contains intelligent handling of change detection to enable us * running in zone-full as well as zone-less mode without any changes to the code. * * @usageNotes * * ### Displaying Observable Values * * ```html * <p>{{ number$ | ngrxPush }}</p> * * <ng-container *ngIf="number$ | ngrxPush as n">{{ n }}</ng-container> * * <app-number [number]="number$ | ngrxPush"></app-number> * ``` * * ### Combining Multiple Observables * * ```html * <code> * {{ { users: users$, query: query$ } | ngrxPush | json }} * </code> * ``` * * @publicApi */ class PushPipe { constructor(errorHandler) { this.errorHandler = errorHandler; this.renderScheduler = createRenderScheduler(); this.renderEventManager = createRenderEventManager({ suspense: (event) => this.setRenderedValue(undefined, event.synchronous), next: (event) => this.setRenderedValue(event.value, event.synchronous), error: (event) => { if (event.reset) { this.setRenderedValue(undefined, event.synchronous); } this.errorHandler.handleError(event.error); }, complete: (event) => { if (event.reset) { this.setRenderedValue(undefined, event.synchronous); } }, }); this.subscription = this.renderEventManager .handlePotentialObservableChanges() .subscribe(); } transform(potentialObservable) { this.renderEventManager.nextPotentialObservable(potentialObservable); return this.renderedValue; } ngOnDestroy() { this.subscription.unsubscribe(); } setRenderedValue(value, isSyncEvent) { if (value !== this.renderedValue) { this.renderedValue = value; if (!isSyncEvent) { this.renderScheduler.schedule(); } } } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: PushPipe, deps: [{ token: i0.ErrorHandler }], target: i0.ɵɵFactoryTarget.Pipe }); } /** @nocollapse */ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.0.0", ngImport: i0, type: PushPipe, isStandalone: true, name: "ngrxPush", pure: false }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: PushPipe, decorators: [{ type: Pipe, args: [{ name: 'ngrxPush', pure: false, }] }], ctorParameters: () => [{ type: i0.ErrorHandler }] }); /** * DO NOT EDIT * * This file is automatically generated at build */ /** * Generated bundle index. Do not edit. */ export { LetDirective, PushPipe, RenderScheduler }; //# sourceMappingURL=ngrx-component.mjs.map