UNPKG

@rx-angular/template

Version:

**Fully** Reactive Component Template Rendering in Angular. @rx-angular/template aims to be a reflection of Angular's built in renderings just reactive.

178 lines (174 loc) 7.29 kB
import * as i0 from '@angular/core'; import { inject, NgZone, untracked, Pipe } from '@angular/core'; import { createTemplateNotifier } from '@rx-angular/cdk/notifications'; import { RxStrategyProvider, strategyHandling } from '@rx-angular/cdk/render-strategies'; import { Subscription, Observable } from 'rxjs'; import { shareReplay, switchMap, skip, tap, withLatestFrom, filter } from 'rxjs/operators'; /** * @Pipe RxPush * * @description * * The push pipe serves as a drop-in replacement for angulars built-in async pipe. * Just like the *rxLet Directive, it leverages a * [RenderStrategy](https://rx-angular.io/docs/cdk/render-strategies) * under the hood which takes care of optimizing the ChangeDetection of your component. The rendering behavior can be * configured per RxPush instance using either a strategy name or provide a * `RxComponentInput` config. * * Usage in the template * * ```html * <hero-search [term]="searchTerm$ | push"> </hero-search> * <hero-list-component [heroes]="heroes$ | push"> </hero-list-component> * ``` * * Using different strategies * * ```html * <hero-search [term]="searchTerm$ | push: 'immediate'"> </hero-search> * <hero-list-component [heroes]="heroes$ | push: 'normal'"> </hero-list-component> * ``` * * Provide a config object * * ```html * <hero-search [term]="searchTerm$ | push: { strategy: 'immediate' }"> </hero-search> * <hero-list-component [heroes]="heroes$ | push: { strategy: 'normal' }"> </hero-list-component> * ``` * * Other Features: * * - lazy rendering (see * [LetDirective](https://github.com/rx-angular/rx-angular/tree/main/libs/template/docs/api/let-directive.md)) * - Take observables or promises, retrieve their values and render the value to the template * - a unified/structured way of handling null, undefined or error * - distinct same values in a row skip not needed re-renderings * * @usageNotes * * ```html * {{observable$ | push}} * <ng-container *ngIf="observable$ | push as o">{{o}}</ng-container> * <component [value]="observable$ | push"></component> * ``` * * @publicApi */ class RxPush { constructor(cdRef) { this.cdRef = cdRef; /** @internal */ this.strategyProvider = inject(RxStrategyProvider); /** @internal */ this.ngZone = inject(NgZone); /** @internal */ this.templateObserver = createTemplateNotifier(); this.templateValues$ = this.templateObserver.values$.pipe(onlyValues(), shareReplay({ bufferSize: 1, refCount: true })); /** @internal */ this.strategyHandler = strategyHandling(this.strategyProvider.primaryStrategy, this.strategyProvider.strategies); } transform(potentialObservable, config, renderCallback) { this._renderCallback = renderCallback; if (config) { if (isRxComponentInput(config)) { this.strategyHandler.next(config.strategy); this._renderCallback = config.renderCallback; // set fallback if patchZone is not set this.setPatchZone(config.patchZone); } else { this.strategyHandler.next(config); } } this.templateObserver.next(potentialObservable); if (!this.subscription) { this.subscription = this.handleChangeDetection(); } return this.renderedValue; } /** @internal */ ngOnDestroy() { untracked(() => this.subscription?.unsubscribe()); } /** @internal */ setPatchZone(patch) { const doPatch = patch == null ? this.strategyProvider.config.patchZone : patch; this.patchZone = doPatch ? this.ngZone : false; } /** @internal */ handleChangeDetection() { const scope = this.cdRef.context; const sub = new Subscription(); // Subscription can be side-effectful, and we don't want any signal reads which happen in the // side effect of the subscription to be tracked by a component's template when that // subscription is triggered via the async pipe. So we wrap the subscription in `untracked` to // decouple from the current reactive context. // // `untracked` also prevents signal _writes_ which happen in the subscription side effect from // being treated as signal writes during the template evaluation (which throws errors). const setRenderedValue = untracked(() => this.templateValues$.subscribe(({ value }) => { this.renderedValue = value; })); const render = untracked(() => this.hasInitialValue(this.templateValues$) .pipe(switchMap((isSync) => this.templateValues$.pipe( // skip ticking change detection // in case we have an initial value, we don't need to perform cd // the variable will be evaluated anyway because of the lifecycle skip(isSync ? 1 : 0), // onlyValues(), this.render(scope), tap((v) => { this._renderCallback?.next(v); })))) .subscribe()); sub.add(setRenderedValue); sub.add(render); return sub; } /** @internal */ render(scope) { return (o$) => o$.pipe(withLatestFrom(this.strategyHandler.strategy$), switchMap(([notification, strategy]) => this.strategyProvider.schedule(() => { strategy.work(this.cdRef, scope); return notification.value; }, { scope, strategy: strategy.name, patchZone: this.patchZone, }))); } /** @internal */ hasInitialValue(value$) { return new Observable((subscriber) => { let hasInitialValue = false; const inner = value$.subscribe(() => { hasInitialValue = true; }); inner.unsubscribe(); subscriber.next(hasInitialValue); subscriber.complete(); }); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxPush, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Pipe }); } /** @nocollapse */ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.0", ngImport: i0, type: RxPush, isStandalone: true, name: "push", pure: false }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxPush, decorators: [{ type: Pipe, args: [{ name: 'push', pure: false, standalone: true }] }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }] }); // https://eslint.org/docs/rules/no-prototype-builtins const hasOwnProperty = Object.prototype.hasOwnProperty; function onlyValues() { return (o$) => o$.pipe(filter((n) => n.kind === "suspense" /* RxNotificationKind.Suspense */ || n.kind === "next" /* RxNotificationKind.Next */)); } function isRxComponentInput(value) { return (value != null && (hasOwnProperty.call(value, 'strategy') || hasOwnProperty.call(value, 'renderCallback') || hasOwnProperty.call(value, 'patchZone'))); } /** * Generated bundle index. Do not edit. */ export { RxPush }; //# sourceMappingURL=template-push.mjs.map