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