@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
JavaScript
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