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.

376 lines (371 loc) 15.1 kB
import * as i0 from '@angular/core'; import { inject, ChangeDetectorRef, NgZone, ViewContainerRef, Injector, isSignal, Directive, Input } 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 { Subscription, ReplaySubject, Subject, merge, NEVER } from 'rxjs'; import { mergeAll, map, filter } from 'rxjs/operators'; const RxIfTemplateNames = { ...RxBaseTemplateNames, then: 'rxThen', else: 'rxElse', }; /** * @Directive IfDirective * @description * * The `RxIf` directive is drop-in replacement for the `NgIf` directive, but with additional features. * `RxIf` allows you to bind observables directly without having the need of using the `async` * pipe in addition. * * This enables `rxIf` to completely operate on its own without having to interact with `NgZone` * or triggering global change detection. * * Read more about the RxIf directive in the [official * docs](https://www.rx-angular.io/docs/template/rx-if-directive). * * @example * <app-item *rxIf="show$"></app-item> * * @docsCategory RxIf * @docsPage RxIf * @publicApi */ class RxIf { /** * @description * * You can change the used `RenderStrategy` by using the `strategy` input of the `*rxIf`. 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-if-directive#use-render-strategies-strategy). * * @example * * \@Component({ * selector: 'app-root', * template: ` * <ng-container *rxIf="showHero$; strategy: 'userBlocking'"> * <app-hero></app-hero> * </ng-container> * * <ng-container *rxIf="showHero$; strategy: strategy$"> * <app-hero></app-hero> * </ng-container> * ` * }) * export class AppComponent { * 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 `*rxIf` rendered a change to the view. * This enables developers to perform actions when rendering has been done. * The `renderCallback` is useful in situations where you * rely on specific DOM properties like the dimensions of an item after it got rendered. * * The `renderCallback` emits the latest value causing the view to update. * * @example * \@Component({ * selector: 'app-root', * template: ` * <app-component> * <app-item * *rxIf=" * show$; * renderCallback: rendered; * " * > * </app-item> * </app-component> * ` * }) * export class AppComponent { * show$ = state.select('showItem'); * // this emits whenever rxIf finished rendering changes * rendered = new Subject<boolean>(); * * constructor(elementRef: ElementRef<HTMLElement>) { * rendered.subscribe(() => { * // item is rendered, we can access its dom now * }) * } * } * * @param {Subject<boolean>} callback */ set renderCallback(callback) { this._renderObserver = callback; } /** @internal */ get thenTemplate() { return this.then ? this.then : this.templateRef; } constructor(templateRef) { this.templateRef = templateRef; /** @internal */ this.strategyProvider = inject(RxStrategyProvider); /** @internal */ this.cdRef = inject(ChangeDetectorRef); /** @internal */ this.ngZone = inject(NgZone); /** @internal */ this.viewContainerRef = inject(ViewContainerRef); /** @internal */ this.injector = inject(Injector); /** @internal */ this.subscription = new Subscription(); /** * @description * * Structural directives maintain `EmbeddedView`s within a components' template. * Depending on the bound value as well as the configured `RxRenderStrategy`, * updates processed by the `*rxIf` directive might be asynchronous. * * Whenever a template gets inserted into, or removed from, its parent component, the directive has to inform the * parent in order to update any view- or contentquery (`@ViewChild`, `@ViewChildren`, `@ContentChild`, * `@ContentChildren`). * * Read more about this in the * [official * docs](https://www.rx-angular.io/docs/template/rx-if-directive#local-strategies-and-view-content-queries-parent). * * @example * \@Component({ * selector: 'app-root', * template: ` * <app-component> * <app-item * *rxIf=" * show$; * parent: true; * " * > * </app-item> * </app-component> * ` * }) * export class AppComponent { * show$ = state.select('showItem'); * } * * @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 `*rxIf` templates are created within `NgZone` or not. * The default value is `true, `*rxIf` will create its `EmbeddedView` inside `NgZone`. * * Event listeners normally trigger zone. * Especially high frequency events can 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-component> * <app-item * *rxIf=" * show$; * patchZone: false; * " * (drag)="itemDrag($event)" * > * </app-item> * </app-component> * ` * }) * export class AppComponent { * show$ = state.select('showItem'); * } * * @param {boolean} patchZone */ this.patchZone = this.strategyProvider.config.patchZone; /** @internal */ this.triggerHandler = new ReplaySubject(1); /** @internal */ this.templateNotifier = createTemplateNotifier(); /** @internal */ this.strategyHandler = coerceAllFactory(() => new ReplaySubject(1), mergeAll()); /** @internal */ this.rendered$ = new Subject(); } /** @internal */ ngOnInit() { 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))); this.subscription.add(this.templateManager .render(this.templateNotifier.values$) .subscribe((n) => { this.rendered$.next(n); this._renderObserver?.next(n); })); } /** @internal */ ngOnChanges(changes) { if (!this.templateManager) { this._createTemplateManager(); } if (changes.then && !changes.then.firstChange) { this.templateManager.addTemplateRef(RxIfTemplateNames.then, this.thenTemplate); } if (changes.else) { this.templateManager.addTemplateRef(RxIfTemplateNames.else, this.else); } if (changes.complete) { this.templateManager.addTemplateRef(RxIfTemplateNames.complete, this.complete); } if (changes.suspense) { this.templateManager.addTemplateRef(RxIfTemplateNames.suspense, this.suspense); this.templateNotifier.withInitialSuspense(!!this.suspense); } if (changes.error) { this.templateManager.addTemplateRef(RxIfTemplateNames.error, this.error); } if (changes.rxIf) { if (isSignal(this.rxIf)) { this.templateNotifier.next(toObservableMicrotaskInternal(this.rxIf, { injector: this.injector })); } else { this.templateNotifier.next(this.rxIf); } } } /** @internal */ ngOnDestroy() { this.subscription.unsubscribe(); } /** @internal */ _createTemplateManager() { const getNextTemplate = (value) => { return value ? RxIfTemplateNames.then : this.else ? RxIfTemplateNames.else : undefined; }; this.templateManager = createTemplateManager({ templateSettings: { viewContainerRef: this.viewContainerRef, customContext: (rxIf) => ({ rxIf }), }, renderSettings: { cdRef: this.cdRef, parent: coerceBooleanProperty(this.renderParent), patchZone: this.patchZone ? this.ngZone : false, defaultStrategyName: this.strategyProvider.primaryStrategy, strategies: this.strategyProvider.strategies, }, notificationToTemplateName: { ["suspense" /* RxNotificationKind.Suspense */]: (value) => this.suspense ? RxIfTemplateNames.suspense : getNextTemplate(value), ["next" /* RxNotificationKind.Next */]: (value) => getNextTemplate(value), ["error" /* RxNotificationKind.Error */]: (value) => this.error ? RxIfTemplateNames.error : getNextTemplate(value), ["complete" /* RxNotificationKind.Complete */]: (value) => this.complete ? RxIfTemplateNames.complete : getNextTemplate(value), }, templateTrigger$: this.triggerHandler, }); this.templateManager.addTemplateRef(RxIfTemplateNames.then, this.thenTemplate); this.templateManager.nextStrategy(this.strategyHandler.values$); } /** * Asserts the correct type of the context for the template that `NgIf` will render. * * The presence of this method is a signal to the Ivy template type-check compiler that the * `NgIf` structural directive renders its template with a specific context type. */ static ngTemplateContextGuard(dir, ctx) { return true; } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxIf, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); } /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.0", type: RxIf, isStandalone: true, selector: "[rxIf]", inputs: { rxIf: "rxIf", strategy: ["rxIfStrategy", "strategy"], else: ["rxIfElse", "else"], then: ["rxIfThen", "then"], suspense: ["rxIfSuspense", "suspense"], complete: ["rxIfComplete", "complete"], error: ["rxIfError", "error"], contextTrigger: ["rxIfContextTrigger", "contextTrigger"], nextTrigger: ["rxIfNextTrigger", "nextTrigger"], suspenseTrigger: ["rxIfSuspenseTrigger", "suspenseTrigger"], errorTrigger: ["rxIfErrorTrigger", "errorTrigger"], completeTrigger: ["rxIfCompleteTrigger", "completeTrigger"], renderParent: ["rxIfParent", "renderParent"], patchZone: ["rxIfPatchZone", "patchZone"], renderCallback: ["rxIfRenderCallback", "renderCallback"] }, usesOnChanges: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxIf, decorators: [{ type: Directive, args: [{ selector: '[rxIf]', standalone: true, }] }], ctorParameters: () => [{ type: i0.TemplateRef }], propDecorators: { rxIf: [{ type: Input }], strategy: [{ type: Input, args: ['rxIfStrategy'] }], else: [{ type: Input, args: ['rxIfElse'] }], then: [{ type: Input, args: ['rxIfThen'] }], suspense: [{ type: Input, args: ['rxIfSuspense'] }], complete: [{ type: Input, args: ['rxIfComplete'] }], error: [{ type: Input, args: ['rxIfError'] }], contextTrigger: [{ type: Input, args: ['rxIfContextTrigger'] }], nextTrigger: [{ type: Input, args: ['rxIfNextTrigger'] }], suspenseTrigger: [{ type: Input, args: ['rxIfSuspenseTrigger'] }], errorTrigger: [{ type: Input, args: ['rxIfErrorTrigger'] }], completeTrigger: [{ type: Input, args: ['rxIfCompleteTrigger'] }], renderParent: [{ type: Input, args: ['rxIfParent'] }], patchZone: [{ type: Input, args: ['rxIfPatchZone'] }], renderCallback: [{ type: Input, args: ['rxIfRenderCallback'] }] } }); /** * @internal * @description * Coerces a data-bound value (typically a string) to a boolean. * */ function coerceBooleanProperty(value) { return value != null && `${value}` !== 'false'; } /** * Generated bundle index. Do not edit. */ export { RxIf, RxIfTemplateNames }; //# sourceMappingURL=template-if.mjs.map