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.

369 lines (365 loc) 15.6 kB
import * as i0 from '@angular/core'; import { inject, ChangeDetectorRef, Injector, NgZone, ViewContainerRef, ErrorHandler, isSignal, Directive, Input, Output } from '@angular/core'; import { coerceAllFactory } from '@rx-angular/cdk/coercing'; import { toObservableMicrotaskInternal } from '@rx-angular/cdk/internals/core'; import { createTemplateNotifier } from '@rx-angular/cdk/notifications'; import { RxStrategyProvider } from '@rx-angular/cdk/render-strategies'; import { RxBaseTemplateNames, createTemplateManager } from '@rx-angular/cdk/template'; import { ReplaySubject, Subscription, Subject, defer, merge, NEVER } from 'rxjs'; import { map, filter } from 'rxjs/operators'; /** @internal */ const RxLetTemplateNames = { ...RxBaseTemplateNames, next: 'nextTpl', }; /** * @Directive RxLet * * @description * In Angular there is one way to handle asynchronous values or streams in the template, the `async` pipe. * Even though the async pipe evaluates such values in the template, it is insufficient in many ways. * To name a few: * * it will only update the template when `NgZone` is also aware of the value change * * it leads to over rendering because it can only run global change detection * * it leads to too many subscriptions in the template * * it is cumbersome to work with values in the template * * read more about the LetDirective in the [official docs](https://www.rx-angular.io/docs/template/let-directive) * * **Conclusion - Structural directives** * * In contrast to global change detection, structural directives allow fine-grained control of change detection on a per directive basis. * The `LetDirective` comes with its own way to handle change detection in templates in a very efficient way. * However, the change detection behavior is configurable on a per directive or global basis. * This makes it possible to implement your own strategies, and also provides a migration path from large existing apps running with Angulars default change detection. * * This package helps to reduce code used to create composable action streams. * It mostly is used in combination with state management libs to handle user interaction and backend communication. * * ```html * <ng-container *rxLet="observableNumber$; let n"> * ... * </ng-container> * ``` * * * @docsCategory LetDirective * @docsPage LetDirective * @publicApi */ class RxLet { /** * @description * * You can change the used `RenderStrategy` by using the `strategy` input of the `*rxLet`. It accepts * an `Observable<RxStrategyNames>` or [`RxStrategyNames`](https://github.com/rx-angular/rx-angular/blob/b0630f69017cc1871d093e976006066d5f2005b9/libs/cdk/render-strategies/src/lib/model.ts#L52). * * The default value for strategy is * [`normal`](https://www.rx-angular.io/docs/template/cdk/render-strategies/strategies/concurrent-strategies). * * Read more about this in the * [official docs](https://www.rx-angular.io/docs/template/let-directive#use-render-strategies-strategy). * * @example * * \@Component({ * selector: 'app-root', * template: ` * <ng-container *rxLet="hero$; let hero; strategy: strategy"> * <app-hero [hero]="hero"></app-hero> * </ng-container> * * <ng-container *rxLet="hero$; let hero; strategy: strategy$"> * <app-hero [hero]="hero"></app-hero> * </ng-container> * ` * }) * export class AppComponent { * strategy = 'low'; * strategy$ = of('immediate'); * } * * @param { string | Observable<string> | undefined } strategyName * @see {@link RxStrategyNames} */ set strategy(strategyName) { this.strategyHandler.next(strategyName); } /** * @description * A `Subject` which emits whenever *rxFor finished rendering a set changes to the view. * This enables developers to perform actions when a list has finished rendering. * The `renderCallback` is useful in situations where you rely on specific DOM properties like the `height` a * table after all items got rendered. * It is also possible to use the renderCallback in order to determine if a view should be visible or not. This * way developers can hide a list as long as it has not finished rendering. * * The result of the `renderCallback` will contain the currently rendered set of items in the iterable. * * @example * \Component({ * selector: 'app-root', * template: ` * <app-list-component> * <app-list-item * *rxFor=" * let item of items$; * trackBy: trackItem; * renderCallback: itemsRendered; * "> * <div>{{ item.name }}</div> * </app-list-item> * </app-list-component> * ` * }) * export class AppComponent { * items$: Observable<Item[]> = itemService.getItems(); * trackItem = (idx, item) => item.id; * // this emits whenever rxFor finished rendering changes * itemsRendered = new Subject<Item[]>(); * * constructor(elementRef: ElementRef<HTMLElement>) { * itemsRendered.subscribe(() => { * // items are rendered, we can now scroll * elementRef.scrollTo({bottom: 0}); * }) * } * } * * @param callback */ set renderCallback(callback) { this._renderObserver = callback; } /** @internal */ static ngTemplateContextGuard(dir, ctx) { return true; } constructor(templateRef) { this.templateRef = templateRef; /** @internal */ this.strategyProvider = inject(RxStrategyProvider); /** @internal */ this.cdRef = inject(ChangeDetectorRef); this.injector = inject(Injector); /** @internal */ this.ngZone = inject(NgZone); /** @internal */ this.viewContainerRef = inject(ViewContainerRef); /** @internal */ this.errorHandler = inject(ErrorHandler); /** * @description * * When local rendering strategies are used, we need to treat view and content queries in a * special way. * To make `*rxLet` in such situations, a certain mechanism is implemented to * execute change detection on the parent (`parent`). * * This is required if your components state is dependent on its view or content children: * * - `@ViewChild` * - `@ViewChildren` * - `@ContentChild` * - `@ContentChildren` * * Read more about this in the * [official * docs](https://www.rx-angular.io/docs/template/let-directive#local-strategies-and-view-content-queries-parent). * * @example * \@Component({ * selector: 'app-root', * template: ` * <app-list-component> * <app-list-item * *rxLet=" * item$; * let item; * parent: true; * " * > * <div>{{ item.name }}</div> * </app-list-item> * </app-list-component> * ` * }) * export class AppComponent { * item$ = itemService.getItem(); * } * * @param boolean * * @deprecated this flag will be dropped soon, as it is no longer required when using signal based view & content queries */ this.renderParent = this.strategyProvider.config.parent; /** * @description * A flag to control whether *rxLet templates are created within `NgZone` or not. * The default value is `true, `*rxLet` will create it's `EmbeddedViews` inside `NgZone`. * * Event listeners normally trigger zone. Especially high frequently events cause performance issues. * * Read more about this in the * [official docs](https://www.rx-angular.io/docs/template/let-directive#working-with-event-listeners-patchzone). * * @example * \@Component({ * selector: 'app-root', * template: ` * <app-list-component> * <app-list-item * *rxLet=" * item$; * let item; * patchZone: false; * " * > * <div>{{ item.name }}</div> * </app-list-item> * </app-list-component> * ` * }) * export class AppComponent { * item$ = itemService.getItem(); * } */ this.patchZone = this.strategyProvider.config.patchZone; /** @internal */ this.observablesHandler = createTemplateNotifier(); /** @internal */ this.strategyHandler = coerceAllFactory(() => new ReplaySubject(1)); /** @internal */ this.triggerHandler = new ReplaySubject(1); /** @internal */ this.subscription = new Subscription(); /** @internal */ this.rendered$ = new Subject(); /** @internal */ this.templateNotification$ = new Subject(); /** @internal */ this.values$ = this.observablesHandler.values$; this.rendered = defer(() => this.rendered$); } /** @internal */ ngOnInit() { this.subscription.add(this.templateManager .render(merge(this.values$, this.templateNotification$)) .subscribe((n) => { this.rendered$.next(n); this._renderObserver?.next(n); })); this.subscription.add(merge(this.contextTrigger || NEVER, this.nextTrigger?.pipe(map(() => "next" /* RxNotificationKind.Next */)) || NEVER, this.suspenseTrigger?.pipe(map(() => "suspense" /* RxNotificationKind.Suspense */)) || NEVER, this.completeTrigger?.pipe(map(() => "complete" /* RxNotificationKind.Complete */)) || NEVER, this.errorTrigger?.pipe(map(() => "error" /* RxNotificationKind.Error */)) || NEVER) .pipe(filter((v) => !!v)) .subscribe((t) => this.triggerHandler.next(t))); } /** @internal */ ngOnChanges(changes) { if (!this.templateManager) { this._createTemplateManager(); } if (changes.complete) { this.templateManager.addTemplateRef(RxLetTemplateNames.complete, this.complete); } if (changes.suspense) { this.templateManager.addTemplateRef(RxLetTemplateNames.suspense, this.suspense); this.observablesHandler.withInitialSuspense(!!this.suspense); } if (changes.error) { this.templateManager.addTemplateRef(RxLetTemplateNames.error, this.error); } if (changes.rxLet) { if (isSignal(this.rxLet)) { this.observablesHandler.next(toObservableMicrotaskInternal(this.rxLet, { injector: this.injector, })); } else { this.observablesHandler.next(this.rxLet); } } } /** @internal */ ngOnDestroy() { this.subscription.unsubscribe(); } /** @internal */ _createTemplateManager() { this.templateManager = createTemplateManager({ templateSettings: { viewContainerRef: this.viewContainerRef, customContext: (rxLet) => ({ rxLet }), }, renderSettings: { cdRef: this.cdRef, parent: !!this.renderParent, patchZone: this.patchZone ? this.ngZone : false, defaultStrategyName: this.strategyProvider.primaryStrategy, strategies: this.strategyProvider.strategies, errorHandler: this.errorHandler, }, notificationToTemplateName: { ["suspense" /* RxNotificationKind.Suspense */]: () => this.suspense ? RxLetTemplateNames.suspense : RxLetTemplateNames.next, ["next" /* RxNotificationKind.Next */]: () => RxLetTemplateNames.next, ["error" /* RxNotificationKind.Error */]: () => this.error ? RxLetTemplateNames.error : RxLetTemplateNames.next, ["complete" /* RxNotificationKind.Complete */]: () => this.complete ? RxLetTemplateNames.complete : RxLetTemplateNames.next, }, templateTrigger$: this.triggerHandler, }); this.templateManager.addTemplateRef(RxLetTemplateNames.next, this.templateRef); this.templateManager.nextStrategy(this.strategyHandler.values$); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxLet, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); } /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.0", type: RxLet, isStandalone: true, selector: "[rxLet]", inputs: { rxLet: "rxLet", strategy: ["rxLetStrategy", "strategy"], complete: ["rxLetComplete", "complete"], error: ["rxLetError", "error"], suspense: ["rxLetSuspense", "suspense"], contextTrigger: ["rxLetContextTrigger", "contextTrigger"], completeTrigger: ["rxLetCompleteTrigger", "completeTrigger"], errorTrigger: ["rxLetErrorTrigger", "errorTrigger"], suspenseTrigger: ["rxLetSuspenseTrigger", "suspenseTrigger"], nextTrigger: ["rxLetNextTrigger", "nextTrigger"], renderCallback: ["rxLetRenderCallback", "renderCallback"], renderParent: ["rxLetParent", "renderParent"], patchZone: ["rxLetPatchZone", "patchZone"] }, outputs: { rendered: "rendered" }, usesOnChanges: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxLet, decorators: [{ type: Directive, args: [{ selector: '[rxLet]', standalone: true }] }], ctorParameters: () => [{ type: i0.TemplateRef }], propDecorators: { rxLet: [{ type: Input }], strategy: [{ type: Input, args: ['rxLetStrategy'] }], complete: [{ type: Input, args: ['rxLetComplete'] }], error: [{ type: Input, args: ['rxLetError'] }], suspense: [{ type: Input, args: ['rxLetSuspense'] }], contextTrigger: [{ type: Input, args: ['rxLetContextTrigger'] }], completeTrigger: [{ type: Input, args: ['rxLetCompleteTrigger'] }], errorTrigger: [{ type: Input, args: ['rxLetErrorTrigger'] }], suspenseTrigger: [{ type: Input, args: ['rxLetSuspenseTrigger'] }], nextTrigger: [{ type: Input, args: ['rxLetNextTrigger'] }], renderCallback: [{ type: Input, args: ['rxLetRenderCallback'] }], renderParent: [{ type: Input, args: ['rxLetParent'] }], patchZone: [{ type: Input, args: ['rxLetPatchZone'] }], rendered: [{ type: Output }] } }); /** * Generated bundle index. Do not edit. */ export { RxLet }; //# sourceMappingURL=template-let.mjs.map