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