@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.
519 lines (510 loc) • 20 kB
JavaScript
import * as i0 from '@angular/core';
import { inject, IterableDiffers, InjectionToken, isSignal, ChangeDetectorRef, NgZone, Injector, ViewContainerRef, ErrorHandler, Directive, Input } from '@angular/core';
import { coerceObservableWith, coerceDistinctWith } from '@rx-angular/cdk/coercing';
import { toObservableMicrotaskInternal } from '@rx-angular/cdk/internals/core';
import { RxStrategyProvider, onStrategy } from '@rx-angular/cdk/render-strategies';
import { isObservable, ReplaySubject, Subscription, combineLatest, concat, of } from 'rxjs';
import { switchAll, shareReplay, startWith, switchMap, ignoreElements, map, catchError } from 'rxjs/operators';
import { RxDefaultListViewContext, createListTemplateManager, RxLiveCollection, reconcile } from '@rx-angular/cdk/template';
class RxForViewContext extends RxDefaultListViewContext {
constructor(item, rxForOf, customProps) {
super(item, customProps);
this.rxForOf = rxForOf;
}
}
const LEGACY_RXFOR_RECONCILIATION_FACTORY = () => {
const iterableDiffers = inject(IterableDiffers);
return (options) => {
const { values$, strategy$, viewContainerRef, template, strategyProvider, errorHandler, createViewContext, updateViewContext, cdRef, trackBy, parent, patchZone, } = options;
const listManager = createListTemplateManager({
iterableDiffers: iterableDiffers,
renderSettings: {
cdRef: cdRef,
strategies: strategyProvider.strategies, // TODO: move strategyProvider
defaultStrategyName: strategyProvider.primaryStrategy,
parent,
patchZone,
errorHandler,
},
templateSettings: {
viewContainerRef,
templateRef: template,
createViewContext,
updateViewContext,
},
trackBy,
});
listManager.nextStrategy(strategy$);
return listManager.render(values$);
};
};
function provideLegacyRxForReconciliation() {
return {
provide: INTERNAL_RX_FOR_RECONCILER_TOKEN,
useFactory: LEGACY_RXFOR_RECONCILIATION_FACTORY,
};
}
/** @internal */
const INTERNAL_RX_FOR_RECONCILER_TOKEN = new InjectionToken('rx-for-reconciler', {
providedIn: 'root',
factory: LEGACY_RXFOR_RECONCILIATION_FACTORY,
});
function injectReconciler() {
return inject(INTERNAL_RX_FOR_RECONCILER_TOKEN);
}
/**
* @Directive RxFor
*
* @description
*
* The most common way to render lists in angular is by using the `*ngFor` structural directive. `*ngFor` is able
* to take an arbitrary list of data and repeat a defined template per item of the list. However, it can
* only do it synchronously.
*
* Compared to the `NgFor`, `RxFor` treats each child template as single renderable unit.
* The change detection of the child templates get prioritized, scheduled and executed by
* leveraging `RenderStrategies` under the hood.
* This technique enables non-blocking rendering of lists and can be referred to as `concurrent mode`.
*
* Read more about this in the [strategies
* section](https://www.rx-angular.io/docs/template/rx-for-directive#rxfor-with-concurrent-strategies).
*
* Furthermore, `RxFor` provides hooks to react to rendered items in form of a `renderCallback: Subject`.
*
* Together with the `RxRenderStrategies`, this makes the rendering behavior extremely versatile
* and transparent for the developer.
* Each instance of `RxFor` can be configured to render with different settings.
*
* Read more in the [official docs](https://www.rx-angular.io/docs/template/rx-for-directive)
*
* @docsCategory RxFor
* @docsPage RxFor
* @publicApi
*/
class RxFor {
/**
* @description
* The iterable input
*
* @example
* <ng-container *rxFor="heroes$; let hero">
* <app-hero [hero]="hero"></app-hero>
* </ng-container>
*
* @param { Observable<(U & NgIterable<T>) | undefined | null>
* | Signal<(U & NgIterable<T>) | undefined | null>
* | (U & NgIterable<T>)
* | null
* | undefined } potentialSignalOrObservable
*/
set rxForOf(potentialSignalOrObservable) {
if (isSignal(potentialSignalOrObservable)) {
this.staticValue = undefined;
this.renderStatic = false;
this.observables$.next(toObservableMicrotaskInternal(potentialSignalOrObservable, {
injector: this.injector,
}));
}
else if (!isObservable(potentialSignalOrObservable)) {
this.staticValue = potentialSignalOrObservable;
this.renderStatic = true;
}
else {
this.staticValue = undefined;
this.renderStatic = false;
this.observables$.next(potentialSignalOrObservable);
}
}
set rxForTemplate(value) {
this._template = value;
}
/**
* @description
*
* You can change the used `RenderStrategy` by using the `strategy` input of the `*rxFor`. 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/rx-for-directive#use-render-strategies-strategy).
*
* @example
*
* \@Component({
* selector: 'app-root',
* template: `
* <ng-container *rxFor="let hero of heroes$; strategy: strategy">
* <app-hero [hero]="hero"></app-hero>
* </ng-container>
*
* <ng-container *rxFor="let hero of heroes$; 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 strategies}
*/
set rxForStrategy(strategyName) {
this.strategyInput$.next(strategyName);
}
/**
* @description
* A function or key that defines how to track changes for items in the iterable.
*
* When items are added, moved, or removed in the iterable,
* the directive must re-render the appropriate DOM nodes.
* To minimize churn in the DOM, only nodes that have changed
* are re-rendered.
*
* By default, rxFor assumes that the object instance identifies the node in the iterable (equality check `===`).
* When a function or key is supplied, rxFor uses the result to identify the item node.
*
* @example
* \@Component({
* selector: 'app-root',
* template: `
* <app-list-component>
* <app-list-item
* *rxFor="
* let item of items$;
* trackBy: 'id';
* "
* >
* <div>{{ item.name }}</div>
* </app-list-item>
* </app-list-component>
* `
* })
* export class AppComponent {
* items$ = itemService.getItems();
* }
*
* // OR
*
* \@Component({
* selector: 'app-root',
* template: `
* <app-list-component>
* <app-list-item
* *rxFor="
* let item of items$;
* trackBy: trackItem;
* "
* >
* <div>{{ item.name }}</div>
* </app-list-item>
* </app-list-component>
* `
* })
* export class AppComponent {
* items$ = itemService.getItems();
* trackItem = (idx, item) => item.id;
* }
*
* @param trackByFnOrKey
*/
set trackBy(trackByFnOrKey) {
if ((typeof ngDevMode === 'undefined' || ngDevMode) &&
trackByFnOrKey != null &&
typeof trackByFnOrKey !== 'string' &&
typeof trackByFnOrKey !== 'function') {
console.warn(`trackBy must be a function, but received ${JSON.stringify(trackByFnOrKey)}.`);
}
if (trackByFnOrKey == null) {
this._trackBy = this.defaultTrackBy;
}
else {
this._trackBy =
typeof trackByFnOrKey !== 'function'
? (i, a) => a[trackByFnOrKey]
: trackByFnOrKey;
}
}
/**
* @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 {Subject<U>} renderCallback
*/
set renderCallback(renderCallback) {
this._renderCallback = renderCallback;
}
get template() {
return this._template || this.templateRef;
}
constructor(templateRef) {
this.templateRef = templateRef;
/** @internal */
this.cdRef = inject(ChangeDetectorRef);
/** @internal */
this.ngZone = inject(NgZone);
/** @internal */
this.injector = inject(Injector);
/** @internal */
this.viewContainerRef = inject(ViewContainerRef);
/** @internal */
this.strategyProvider = inject(RxStrategyProvider);
/** @internal */
this.errorHandler = inject(ErrorHandler);
/** @internal */
this.renderStatic = false;
/**
* @description
*
* When local rendering strategies are used, we need to treat view and content queries in a
* special way.
* To make `*rxFor` 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/rx-for-directive#local-strategies-and-view-content-queries-parent).
*
* @example
* \@Component({
* selector: 'app-root',
* template: `
* <app-list-component>
* <app-list-item
* *rxFor="
* let item of items$;
* trackBy: trackItem;
* parent: true;
* "
* >
* <div>{{ item.name }}</div>
* </app-list-item>
* </app-list-component>
* `
* })
* export class AppComponent {
* items$ = itemService.getItems();
* }
*
* @param {boolean} renderParent
*
* @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 *rxFor templates are created within `NgZone` or not.
* The default value is `true, `*rxFor` 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/rx-for-directive#working-with-event-listeners-patchzone).
*
* @example
* \@Component({
* selector: 'app-root',
* template: `
* <app-list-component>
* <app-list-item
* *rxFor="
* let item of items$;
* trackBy: trackItem;
* patchZone: false;
* "
* >
* <div>{{ item.name }}</div>
* </app-list-item>
* </app-list-component>
* `
* })
* export class AppComponent {
* items$ = itemService.getItems();
* }
*
* @param {boolean} patchZone
*/
this.patchZone = this.strategyProvider.config.patchZone;
this.defaultTrackBy = (i, item) => item;
/** @internal */
this.strategyInput$ = new ReplaySubject(1);
/** @internal */
this.observables$ = new ReplaySubject(1);
/** @internal */
this.values$ = this.observables$.pipe(coerceObservableWith(), switchAll(), shareReplay({ refCount: true, bufferSize: 1 }));
/** @internal */
this.values = null;
/** @internal */
this.strategy$ = this.strategyInput$.pipe(coerceDistinctWith());
/** @internal */
this._subscription = new Subscription();
/** @internal */
this._trackBy = this.defaultTrackBy;
/** @internal */
this._distinctBy = (a, b) => a === b;
this.reconciler = injectReconciler();
}
/** @internal */
ngOnInit() {
this._subscription.add(this.values$.subscribe((v) => (this.values = v)));
this._subscription.add(this.reconciler({
values$: this.values$,
strategy$: this.strategy$,
viewContainerRef: this.viewContainerRef,
template: this.template,
strategyProvider: this.strategyProvider,
errorHandler: this.errorHandler,
cdRef: this.cdRef,
trackBy: this._trackBy,
createViewContext: this.createViewContext.bind(this),
updateViewContext: this.updateViewContext.bind(this),
parent: !!this.renderParent,
patchZone: this.patchZone ? this.ngZone : undefined,
}).subscribe((values) => this._renderCallback?.next(values)));
}
/** @internal */
createViewContext(item, computedContext) {
return new RxForViewContext(item, this.values, computedContext);
}
/** @internal */
updateViewContext(item, view, computedContext) {
view.context.updateContext(computedContext);
view.context.rxForOf = this.values;
view.context.$implicit = item;
}
/** @internal */
ngDoCheck() {
if (this.renderStatic) {
this.observables$.next(this.staticValue);
}
}
/** @internal */
ngOnDestroy() {
this._subscription.unsubscribe();
this.viewContainerRef.clear();
}
/** @internal */
static ngTemplateContextGuard(dir, ctx) {
return true;
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxFor, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); }
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.0", type: RxFor, isStandalone: true, selector: "[rxFor][rxForOf]", inputs: { rxForOf: "rxForOf", rxForTemplate: "rxForTemplate", rxForStrategy: "rxForStrategy", renderParent: ["rxForParent", "renderParent"], patchZone: ["rxForPatchZone", "patchZone"], trackBy: ["rxForTrackBy", "trackBy"], renderCallback: ["rxForRenderCallback", "renderCallback"] }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxFor, decorators: [{
type: Directive,
args: [{
selector: '[rxFor][rxForOf]',
standalone: true,
}]
}], ctorParameters: () => [{ type: i0.TemplateRef }], propDecorators: { rxForOf: [{
type: Input
}], rxForTemplate: [{
type: Input
}], rxForStrategy: [{
type: Input
}], renderParent: [{
type: Input,
args: ['rxForParent']
}], patchZone: [{
type: Input,
args: ['rxForPatchZone']
}], trackBy: [{
type: Input,
args: ['rxForTrackBy']
}], renderCallback: [{
type: Input,
args: ['rxForRenderCallback']
}] } });
function provideExperimentalRxForReconciliation() {
return {
provide: INTERNAL_RX_FOR_RECONCILER_TOKEN,
useFactory: () => (options) => {
const { values$, strategy$, viewContainerRef, template, strategyProvider, errorHandler, createViewContext, updateViewContext, cdRef, trackBy, parent, patchZone, } = options;
const liveCollection = new RxLiveCollection(viewContainerRef, template, strategyProvider, createViewContext, updateViewContext);
return combineLatest([
values$,
strategy$.pipe(startWith(strategyProvider.primaryStrategy)),
]).pipe(switchMap(([iterable, strategyName]) => {
if (iterable == null) {
iterable = [];
}
if (!iterable[Symbol.iterator]) {
throw new Error(`Error trying to diff '${iterable}'. Only arrays and iterables are allowed`);
}
const strategy = strategyProvider.strategies[strategyName]
? strategyName
: strategyProvider.primaryStrategy;
liveCollection.reset();
reconcile(liveCollection, iterable, trackBy);
liveCollection.updateIndexes();
return liveCollection.flushQueue(strategy).pipe((o$) => parent && liveCollection.needHostUpdate
? concat(o$, onStrategy(null, strategyProvider.strategies[strategy], (_, work, options) => {
work(cdRef, options.scope);
}, {
scope: cdRef.context ?? cdRef,
ngZone: patchZone,
}).pipe(ignoreElements()))
: o$, map(() => iterable));
}), catchError((e) => {
errorHandler.handleError(e);
return of(null);
}));
},
};
}
/**
* Generated bundle index. Do not edit.
*/
export { RxFor, RxForViewContext, provideExperimentalRxForReconciliation, provideLegacyRxForReconciliation };
//# sourceMappingURL=template-for.mjs.map