UNPKG

@bespunky/angular-zen

Version:

The Angular tools you always wished were there.

1,069 lines (1,052 loc) 142 kB
import * as i0 from '@angular/core'; import { InjectionToken, Injectable, Inject, PLATFORM_ID, Directive, EventEmitter, Output, Input, NgModule, ElementRef } from '@angular/core'; import { DOCUMENT as DOCUMENT$1, isPlatformBrowser } from '@angular/common'; import { Subject, Subscription, BehaviorSubject, EMPTY, combineLatest, concat, forkJoin, merge, of, animationFrames, interval } from 'rxjs'; import { map, share, tap, switchMap, materialize, finalize, startWith, delay, takeWhile, filter, mapTo } from 'rxjs/operators'; /** A token used to provide the native document implementation for `DocumentRef`. By default, `CoreModule` will provide angular's DOCUMENT token. */ const DOCUMENT = new InjectionToken('DocumentToken'); /** * Provides an injectable wrapper for the `document` object. * Inject this in your services/components and you will be able to easily mock or spy on the native `document` object in your tests. * * By default, the `nativeDocument` property will point to angular's DOM adapter, thus facilitating DOM access and manipulation * on the different platforms. * To mock the native document, provide a value for the `DOCUMENT` token from `@bespunky/angular-zen/core`. * You will safely mock it without trashing angular's `DOCUMENT` provider. * * @see document-ref.service.spec.ts for examples. */ class DocumentRef { // Treating native document as `any` save users typecasting everytime and deducing if the object is of type `Document` or `object`. /** * Creates an instance of `DocumentRef`. * * @param {*} nativeDocument The native document provided by the `DOCUMENT` token of `@bespunky/angular-zen/core`. See `DocumentRef` for details. */ constructor(nativeDocument) { this.nativeDocument = nativeDocument; } } DocumentRef.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: DocumentRef, deps: [{ token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Injectable }); DocumentRef.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: DocumentRef, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: DocumentRef, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return [{ type: undefined, decorators: [{ type: Inject, args: [DOCUMENT] }] }]; } }); /** * The default provider for the `DOCUMENT` token. Uses angular's DOM adapters which will be injected according to the platform. */ const DocumentProvider = { provide: DOCUMENT, useExisting: DOCUMENT$1 }; /** * A bundle of all providers needed for DocumentRef to work. */ const DocumentRefProviders = [DocumentProvider]; /** * An injectable token that will allow us to replace the provider for the native window object when necessary (e.g. mocking the `window` object). */ const WINDOW = new InjectionToken('WindowToken'); /** * Provides an injectable wrapper for the `window` object. * * Inject this in your services/components and you will be able to easily mock or spy on the native `window` object in your tests. * You can replace the default `WINDOW` token provider, which allows you to mock the `window` object. * * @see window-ref.service.spec.ts for examples. */ class WindowRef { // Treating native window as `any` save users typecasting everytime and deducing if the object is of type `Window` or `object`. /** * Creates an instance of WindowRef. * * @param {*} nativeWindow The native window provided by the `WINDOW` token of `@bespunky/angular-zen/core`. See `WindowRef` for details. */ constructor(nativeWindow) { this.nativeWindow = nativeWindow; } } WindowRef.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: WindowRef, deps: [{ token: WINDOW }], target: i0.ɵɵFactoryTarget.Injectable }); WindowRef.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: WindowRef, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: WindowRef, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return [{ type: undefined, decorators: [{ type: Inject, args: [WINDOW] }] }]; } }); /** * Provides a platform dependant implementation for retrieving the `window` object. * * @returns `window` for browser platforms and a new object for non-browser platforms. */ function windowFactory(platformId) { return isPlatformBrowser(platformId) ? window : new Object(); } /** * The default provider for the `WINDOW` token. Provides `window` for browser platforms and a new object for non-browser platforms. */ const WindowProvider = { provide: WINDOW, useFactory: windowFactory, deps: [PLATFORM_ID] }; /** * A bundle of all providers needed for WindowRef to work. */ const WindowRefProviders = [WindowProvider]; /** * Facilitates working with components, directives and services which manually subscribe to observables. * Extend this class to easily hook into ngOnDestroy and avoid memory leaks. * * @see [Wiki](https://bs-angular-zen.web.app/docs/zen/additional-documentation/coremodule/destroyable-(abstract).html) for full guide. * * @export * @abstract * @class Destroyable * @implements {OnDestroy} */ class Destroyable { constructor() { /** * Emits a value when `ngOnDestroy()` is called. * Pipe together with `takeUntil()` to auto unsubscribe from your observables. * * @example * observable.pipe(takeUntil(this.destroyed)).subscribe(...); * * @protected * @type {Subject<void>} */ this.destroyed = new Subject(); /** * A list of all subscriptions manually added using the `subscribe()` method. * These are automatically unsubscribed when `ngOnDestroy()` is called. * * @protected * @type {Subscription} */ this.subscriptions = new Subscription(); } ngOnDestroy() { this.destroyed.next(); this.destroyed.complete(); this.subscriptions.unsubscribe(); } subscribe(observable, observerOrNext, error, complete) { // Cast partial observer object const observer = observerOrNext instanceof Function ? { next: observerOrNext, error, complete } : observerOrNext; this.subscriptions.add(observable.subscribe(observer)); return this.subscriptions; } } Destroyable.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: Destroyable, deps: [], target: i0.ɵɵFactoryTarget.Directive }); Destroyable.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.2.12", type: Destroyable, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: Destroyable, decorators: [{ type: Directive }] }); /** * The base class for all `*observeXXX` directives. * This directive bind an observable or a collection of observables with a template, causing the values in the template to be updated whenever the observables emit. * * Any template assigned with the directive will render immediately, and its view context will be updated with the emitted value on * each emission. The directive will be responsible for subscribing on init and unsubscribing on destroy. * * ## Features * * #### Shared observable * The watched observable will automatically be multicasted so that any child observables created by the template will use the same * stream. * * The shared observable can be accessed using the `let source = source` microsyntax. * * #### Observer events * Whenever the observable changes state or emits a value, the corresponding event is emitted: * `nextCalled` - A value has been emitted. `$event` will be the emitted value. * `errorCalled` - An error has occured in the pipeline. `$event` will be the error. * `completeCalled` - The observable has completed. `$event` will be void. * * > Because of limitations to Angular's Structural Directives, in order to bind the events the desugared syntax must be used. * This, for example, **will trigger** the event: * > ```html * ><ng-template [observe]="x$" let-source="source" (nextCalled)="onNext($event)"> * > ... * ></ng-template> * > ``` * > * >This **will NOT trigger** the event: * >```html * > <div *observe="x$; let source = source" (nextCalled)="onNext($event)">...</div> * >``` * * ## ⚠️ Extending notes: * As the base class cannot deduce the directive selector (e.g. `observeLatest`, `observeMerge`, etc.) the extending class * is required to do 4 things: * 1. Implement the abstract `selector` member and assign it with the directive's selector. * 2. Implement and `@Input() public set <selector>(value: T)` which will pass its value to the `input` member. * 3. Implement a static context TypeGuard. * * These will enable Angular features like template type checking and the microsyntax `as` keyword. * * @export * @abstract * @class ObserveBaseDirective * @extends {Destroyable} * @implements {OnInit} * @template TInput The type of value this directive will work with. Depends on the extending class. * @template TResolved The type of value emitted by the observable. Depends on the extending class. * @template TContext The type of the context object the template will work with. */ class ObserveBaseDirective extends Destroyable { constructor(template, viewContainer) { super(); this.template = template; this.viewContainer = viewContainer; /** * Triggered whenever the observable emits a value. `$event` will be the emitted value. * * @type {EventEmitter<TResolved>} */ this.nextCalled = new EventEmitter(); /** * Triggered when an error occurs in the observable's pipeline. `$event` will be the error. * * @type {EventEmitter<unknown>} */ this.errorCalled = new EventEmitter(); /** * Triggered when the observable completes. `$event` will be the void. * * @type {EventEmitter<void>} */ this.completeCalled = new EventEmitter(); /** * ### Why BehaviorSubject<{@link TInput s} | null> and not Subject<{@link TInput}> * `input` is set from @Input properties. For some reason, Angular passes-in the first value BEFORE * ngOnInit, even though other @Input properties (e.g. showAfter, showFor) are passed AFTER ngOnInit. * If subscription occurs in the constructor, `input` will emit the first observable too fast, which * might lead to pipes breaking or misbehaving if they rely on properties to be instantiated first. * * This leads to subscribing in ngOnInit, to allow Angular time to initialize those. * BUT, if `input` is a Subject, as the first value was already emitted BEFORE ngOnInit, it will not be * captured by our subscription to `input`. Hence the BehaviorSubject - To allow capturing that first observable. */ this.input = new BehaviorSubject(null); this.renderView(); } ngOnInit() { // See `this.input` documentation for why subscription is done in ngOnInit. this.subscribe(this.contextFeed()); } contextFeed() { return this.input.pipe( // Whenever a new value is provided into the directive use the extender's implementation to observe it and multicast it. map(input => input ? this.observeInput(input).pipe(share()) : EMPTY), // Replace the source observable in the context with the newly created observable. tap(source => this.updateViewContext({ source })), // Switch to the new observable and materialize it to watch for state changes and emit events accordingly switchMap(source => source.pipe(materialize())), // Whenever a materialized notification is emitted, handle it and emit the relevant event tap(meta => this.onStateChange(meta))); } onStateChange(meta) { // Call the appropriate handler according to the received notification return meta.observe({ next: value => { this.updateViewContext({ value }); this.nextCalled.emit(value); }, error: error => this.errorCalled.emit(error), complete: () => this.completeCalled.emit() }); } renderView() { const context = this.createViewContext({}); this.view = this.viewContainer.createEmbeddedView(this.template, context); } updateViewContext(data) { this.view.context = this.createViewContext(data); } createViewContext({ value, source }) { value ?? (value = this.view?.context.$implicit || null); source ?? (source = this.view?.context.source || EMPTY); return { $implicit: value, [this.selector]: value, source }; } } ObserveBaseDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveBaseDirective, deps: [{ token: i0.TemplateRef }, { token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Directive }); ObserveBaseDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.2.12", type: ObserveBaseDirective, outputs: { nextCalled: "nextCalled", errorCalled: "errorCalled", completeCalled: "completeCalled" }, usesInheritance: true, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveBaseDirective, decorators: [{ type: Directive }], ctorParameters: function () { return [{ type: i0.TemplateRef }, { type: i0.ViewContainerRef }]; }, propDecorators: { nextCalled: [{ type: Output }], errorCalled: [{ type: Output }], completeCalled: [{ type: Output }] } }); /** * Documentation in {@link ObserveDirective.observe} to allow in-template tooltips. * * @export * @class ObserveDirective * @extends {ObserveBaseDirective<T, EmittedTypeOf<T>, ObserveContext<T>>} * @template T The type of observable received by the directive. */ class ObserveDirective extends ObserveBaseDirective { constructor() { super(...arguments); this.selector = 'observe'; } /** * Tracks an observable and updates the template with its emitted value on each emission. * * Any template assigned with the directive will render immediately, and its view context will be updated with the emitted value on * each emission. The directive will be responsible for subscribing on init and unsubscribing on destroy. * * ## Features * * #### Shared observable * The watched observable will automatically be multicasted so that any child observables created by the template will use the same * stream. * * The shared observable can be accessed using the `let source = source` microsyntax. * * #### Observer events * Whenever the observable changes state or emits a value, the corresponding event is emitted: * `nextCalled` - A value has been emitted. `$event` will be the emitted value. * `errorCalled` - An error has occured in the pipeline. `$event` will be the error. * `completeCalled` - The observable has completed. `$event` will be void. * * > Because of limitations to Angular's Structural Directives, in order to bind the events the desugared syntax must be used. * This, for example, **will trigger** the event: * > ```html * ><ng-template [observe]="x$" let-source="source" (nextCalled)="onNext($event)"> * > ... * ></ng-template> * > ``` * > * >This **will NOT trigger** the event: * >```html * > <div *observe="x$; let source = source" (nextCalled)="onNext($event)">...</div> * >``` */ set observe(value) { this.input.next(value); } static ngTemplateContextGuard(directive, context) { return true; } observeInput(input) { return input; } } ObserveDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); ObserveDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.2.12", type: ObserveDirective, selector: "[observe]", inputs: { observe: "observe" }, usesInheritance: true, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveDirective, decorators: [{ type: Directive, args: [{ selector: '[observe]' }] }], propDecorators: { observe: [{ type: Input }] } }); /** * The base class for `*observe` directives combining observables using a map of observable values (i.e. { x: x$, y: y$ }). * * Emitted values will be available as the implicit context values but will also be spread into the context by key. * Meaning, this would work: * ```html * <div *observeXXX="{ x: x$, y: y$ } as result">{{result.x}}</div> * ``` * * And this also: * ``` * <div *observeXXX="{ x: x$, y: y$ }; let x = x">{{x}}</div> * ``` * * @export * @abstract * @class ObserveMapDirective * @extends {ObserveBaseDirective<TInput, EmittedMapOf<TInput>, TContext>} * @template TInput The type of observable map. * @template TContext The the of context the directive will provide to the view. */ class ObserveMapDirective extends ObserveBaseDirective { createViewContext(data) { // Spread the values emitted from the observable to allow `let` microsyntax and directly accessing them return { ...super.createViewContext(data), ...data.value }; } } ObserveMapDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveMapDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); ObserveMapDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.2.12", type: ObserveMapDirective, usesInheritance: true, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveMapDirective, decorators: [{ type: Directive }] }); /** * Documentation in {@link ObserveLatestDirective.observeLatest} to allow in-template tooltips. * @export * @class ObserveLatestDirective * @extends {ObserveMapDirective<T, ObserveLatestContext<T>>} * @template T The type of observables map received by the directive. */ class ObserveLatestDirective extends ObserveMapDirective { constructor() { super(...arguments); // Seems like for template typechecking to work with a generic type holding `unknown`, the generic type must be flattened // and not represented by a new type. When I tried creating an ObservableMap = { ...key: Observable<unknown> } and use it // as T extends ObservableMap, the type system failed to infer the inner types of the observables. // T extends { ...key: Observable<unknown> } works fine. this.selector = 'observeLatest'; } /** * Combines a map of observables using rxjs {@link https://rxjs.dev/api/index/function/combineLatest combineLatest()} and exposes the emitted values to the template. * Values are exposed in a map which keys are the same keys as the original observable map and values are * the emitted values corresponding to those keys. * * Emitted values will be available as the implicit context values but will also be spread into the context by key. * Meaning, this would work: * ```html * <div *observeLatest="{ x: x$, y: y$ } as result">{{result.x}}</div> * ``` * * And this also: * ``` * <div *observeLatest="{ x: x$, y: y$ }; let x = x">{{x}}</div> * ``` * * Any template assigned with the directive will render immediately, and its view context will be updated with the emitted value on * each emission. The directive will be responsible for subscribing on init and unsubscribing on destroy. * * ## Features * * #### Shared observable * The watched observable will automatically be multicasted so that any child observables created by the template will use the same * stream. * * The shared observable can be accessed using the `let source = source` microsyntax. * * #### Observer events * Whenever the observable changes state or emits a value, the corresponding event is emitted: * `nextCalled` - A value has been emitted. `$event` will be the emitted value. * `errorCalled` - An error has occured in the pipeline. `$event` will be the error. * `completeCalled` - The observable has completed. `$event` will be void. * * > Because of limitations to Angular's Structural Directives, in order to bind the events the desugared syntax must be used. * This, for example, **will trigger** the event: * > ```html * ><ng-template [observe]="x$" let-source="source" (nextCalled)="onNext($event)"> * > ... * ></ng-template> * > ``` * > * >This **will NOT trigger** the event: * >```html * > <div *observe="x$; let source = source" (nextCalled)="onNext($event)">...</div> * >``` */ set observeLatest(value) { this.input.next(value); } static ngTemplateContextGuard(directive, context) { return true; } observeInput(input) { return combineLatest(input); } } ObserveLatestDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveLatestDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); ObserveLatestDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.2.12", type: ObserveLatestDirective, selector: "[observeLatest]", inputs: { observeLatest: "observeLatest" }, usesInheritance: true, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveLatestDirective, decorators: [{ type: Directive, args: [{ selector: '[observeLatest]' }] }], propDecorators: { observeLatest: [{ type: Input }] } }); /** * The base class for `*observe` directives combining observables using an array of observable values (i.e. [x$, y$]). * * @export * @abstract * @class ObserveArrayDirective * @extends {ObserveBaseDirective<TInput, TResolved, TContext>} * @template TInput The type of observable array. * @template TResolved The type of resolved array. * @template TContext The the of context the directive will provide to the view. */ class ObserveArrayDirective extends ObserveBaseDirective { } ObserveArrayDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveArrayDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); ObserveArrayDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.2.12", type: ObserveArrayDirective, usesInheritance: true, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveArrayDirective, decorators: [{ type: Directive }] }); /** * Documentation in {@link ObserveConcatDirective.observeConcat} to allow in-template tooltips. * * @export * @class ObserveConcatDirective * @extends {ObserveArrayDirective<T, EmittedArrayTypesOf<T>, ObserveConcatContext<T>>} * @template T The type of observables tuple received by the directive. */ class ObserveConcatDirective extends ObserveArrayDirective { constructor() { super(...arguments); this.selector = 'observeConcat'; } /** * Concats an array of observables using rxjs {@link https://rxjs.dev/api/index/function/concat concat()} and exposes the emitted values to the template. * * Any template assigned with the directive will render immediately, and its view context will be updated with the emitted value on * each emission. The directive will be responsible for subscribing on init and unsubscribing on destroy. * * ## Features * * #### Shared observable * The watched observable will automatically be multicasted so that any child observables created by the template will use the same * stream. * * The shared observable can be accessed using the `let source = source` microsyntax. * * #### Observer events * Whenever the observable changes state or emits a value, the corresponding event is emitted: * `nextCalled` - A value has been emitted. `$event` will be the emitted value. * `errorCalled` - An error has occured in the pipeline. `$event` will be the error. * `completeCalled` - The observable has completed. `$event` will be void. * * > Because of limitations to Angular's Structural Directives, in order to bind the events the desugared syntax must be used. * This, for example, **will trigger** the event: * > ```html * ><ng-template [observe]="x$" let-source="source" (nextCalled)="onNext($event)"> * > ... * ></ng-template> * > ``` * > * >This **will NOT trigger** the event: * >```html * > <div *observe="x$; let source = source" (nextCalled)="onNext($event)">...</div> * >``` */ set observeConcat(value) { this.input.next(value); } static ngTemplateContextGuard(directive, context) { return true; } observeInput(input) { return concat(...input); } } ObserveConcatDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveConcatDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); ObserveConcatDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.2.12", type: ObserveConcatDirective, selector: "[observeConcat]", inputs: { observeConcat: "observeConcat" }, usesInheritance: true, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveConcatDirective, decorators: [{ type: Directive, args: [{ selector: '[observeConcat]' }] }], propDecorators: { observeConcat: [{ type: Input }] } }); /** * Documentation in {@link ObserveJoinDirective.observeJoin} to allow in-template tooltips. * @export * @class ObserveJoinDirective * @extends {ObserveMapDirective<T, ObserveJoinContext<T>>} * @template T The type of observables map received by the directive. */ class ObserveJoinDirective extends ObserveMapDirective { constructor() { super(...arguments); this.selector = 'observeJoin'; } /** * Joins a map of observables using rxjs {@link https://rxjs.dev/api/index/function/forkJoin forkJoin()} and exposes the emitted values to the template. * Values are exposed in a map which keys are the same keys as the original observable map and values are * the emitted values corresponding to those keys. * * Emitted values will be available as the implicit context values but will also be spread into the context by key. * Meaning, this would work: * ```html * <div *observeJoin="{ x: x$, y: y$ } as result">{{result.x}}</div> * ``` * * And this also: * ``` * <div *observeJoin="{ x: x$, y: y$ }; let x = x">{{x}}</div> * ``` * * Any template assigned with the directive will render immediately, and its view context will be updated with the emitted value on * each emission. The directive will be responsible for subscribing on init and unsubscribing on destroy. * * ## Features * * #### Shared observable * The watched observable will automatically be multicasted so that any child observables created by the template will use the same * stream. * * The shared observable can be accessed using the `let source = source` microsyntax. * * #### Observer events * Whenever the observable changes state or emits a value, the corresponding event is emitted: * `nextCalled` - A value has been emitted. `$event` will be the emitted value. * `errorCalled` - An error has occured in the pipeline. `$event` will be the error. * `completeCalled` - The observable has completed. `$event` will be void. * * > Because of limitations to Angular's Structural Directives, in order to bind the events the desugared syntax must be used. * This, for example, **will trigger** the event: * > ```html * ><ng-template [observe]="x$" let-source="source" (nextCalled)="onNext($event)"> * > ... * ></ng-template> * > ``` * > * >This **will NOT trigger** the event: * >```html * > <div *observe="x$; let source = source" (nextCalled)="onNext($event)">...</div> * >``` */ set observeJoin(value) { this.input.next(value); } static ngTemplateContextGuard(directive, context) { return true; } observeInput(input) { return forkJoin(input); } } ObserveJoinDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveJoinDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); ObserveJoinDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.2.12", type: ObserveJoinDirective, selector: "[observeJoin]", inputs: { observeJoin: "observeJoin" }, usesInheritance: true, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveJoinDirective, decorators: [{ type: Directive, args: [{ selector: '[observeJoin]' }] }], propDecorators: { observeJoin: [{ type: Input }] } }); /** * Documentation in {@link ObserveMergeDirective.observeMerge} to allow in-template tooltips. * * @export * @class ObserveMergeDirective * @extends {ObserveArrayDirective<T, EmittedArrayTypesOf<T>, ObserveMergeContext<T>>} * @template T The type of observables tuple received by the directive. */ class ObserveMergeDirective extends ObserveArrayDirective { constructor() { super(...arguments); this.selector = 'observeMerge'; } /** * Combines an array of observables using rxjs {@link https://rxjs.dev/api/index/function/merge merge()} and exposes the emitted values to the template. * * Any template assigned with the directive will render immediately, and its view context will be updated with the emitted value on * each emission. The directive will be responsible for subscribing on init and unsubscribing on destroy. * * ## Features * * #### Shared observable * The watched observable will automatically be multicasted so that any child observables created by the template will use the same * stream. * * The shared observable can be accessed using the `let source = source` microsyntax. * * #### Observer events * Whenever the observable changes state or emits a value, the corresponding event is emitted: * `nextCalled` - A value has been emitted. `$event` will be the emitted value. * `errorCalled` - An error has occured in the pipeline. `$event` will be the error. * `completeCalled` - The observable has completed. `$event` will be void. * * > Because of limitations to Angular's Structural Directives, in order to bind the events the desugared syntax must be used. * This, for example, **will trigger** the event: * > ```html * ><ng-template [observe]="x$" let-source="source" (nextCalled)="onNext($event)"> * > ... * ></ng-template> * > ``` * > * >This **will NOT trigger** the event: * >```html * > <div *observe="x$; let source = source" (nextCalled)="onNext($event)">...</div> * >``` */ set observeMerge(value) { this.input.next(value); } static ngTemplateContextGuard(directive, context) { return true; } observeInput(input) { return merge(...input); } } ObserveMergeDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveMergeDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); ObserveMergeDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.2.12", type: ObserveMergeDirective, selector: "[observeMerge]", inputs: { observeMerge: "observeMerge" }, usesInheritance: true, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveMergeDirective, decorators: [{ type: Directive, args: [{ selector: '[observeMerge]' }] }], propDecorators: { observeMerge: [{ type: Input }] } }); /** * Provides directives to facilitate in-template subscription management to observables. * * @export * @class ObserveModule */ class ObserveModule { } ObserveModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); ObserveModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "14.2.12", ngImport: i0, type: ObserveModule, declarations: [ObserveDirective, ObserveLatestDirective, ObserveJoinDirective, ObserveMergeDirective, ObserveConcatDirective], exports: [ObserveDirective, ObserveLatestDirective, ObserveJoinDirective, ObserveMergeDirective, ObserveConcatDirective] }); ObserveModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveModule }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.12", ngImport: i0, type: ObserveModule, decorators: [{ type: NgModule, args: [{ declarations: [ ObserveDirective, ObserveLatestDirective, ObserveJoinDirective, ObserveMergeDirective, ObserveConcatDirective, ], exports: [ ObserveDirective, ObserveLatestDirective, ObserveJoinDirective, ObserveMergeDirective, ObserveConcatDirective, ] }] }] }); const DurationMultipliers = { ms: 1, s: 1000, m: 60000 }; function durationToMs(duration) { if (typeof duration === 'number') return duration; const regex = /(?<value>\d+(.\d+)?)(?<units>\w+)/; const { value, units } = duration.match(regex)?.groups; return parseInt(value) * (DurationMultipliers[units] || 1); } function breakdownTime(showingForMs) { const dummyDate = new Date(showingForMs); const showingFor = { m: dummyDate.getMinutes(), s: dummyDate.getSeconds(), ms: dummyDate.getMilliseconds(), totalMinutes: showingForMs / DurationMultipliers.m, totalSeconds: showingForMs / DurationMultipliers.s, totalMilliseconds: showingForMs, }; return showingFor; } /** * Represents the context to be fed into a view rendered by an {@link OnObserverBaseDirective `*onObserver`} directive. * The context is immutable. * * @export * @class OnObserverContext * @template TResolved The type of value emitted by the observable the directive is observing. */ class OnObserverContext { /** * Creates an instance of OnObserverContext. */ constructor( /** * The selector of the directive which is creating this context. This will be used to assign the emitted value to a * property matching the selector, thus enabling the use of the microsyntax `as` keyword. */ selector, /** The index of the view rendered by the directive. If the directive is in `'single'` view mode, this will always be 0. */ index, /** The name of the observer call which triggered this context creation. */ call, /** (Optional) The value, if any, that was emitted by the original observable. */ value, /** * (Optional) The time left for the view to be rendered. Only used when {@link OnObserverBaseDirective.showFor `showFor`} * is specified in the directive. */ remaining, /** * @deprecated Use {@link OnObserverContext.remaining `remaining`} instead. This will be removed in v6.0.0. */ showingFor, /** * (Optional) The time elapsed from the moment the view was rendered. Only used when {@link OnObserverBaseDirective.showFor `showFor`} * is specified in the directive. */ elapsed) { this.index = index; this.call = call; this.remaining = remaining; this.showingFor = showingFor; this.elapsed = elapsed; this.$implicit = this[selector] = value; } /** * Creates a context object for the specified view render commitment. * * @static * @template T The type of value emitted by the observable. * @param {string} onObserverSelector The selector of the directive which is creating this context. * @param {number} index The index of the view rendered by the directive. * @param {ViewRenderCommitment<T>} commitment The view render commitment from which to create the context. * @return {OnObserverContext<T>} A context object for the specified view render commitment. */ static fromCommitment(onObserverSelector, index, { call: { name, value } }) { return new OnObserverContext(onObserverSelector, index, name, value); } } ; /** Maps RxJS materialized notification states to their observer handler name. */ const StateNotificationMap = { N: 'next', E: 'error', C: 'complete' }; /** * Represents an intercepted observer call made by a materialized observable. * * @export * @class ObserverCall * @template T The type of value emitted by the materialized observable. */ class ObserverCall { /** * Creates an instance of ObserverCall. */ /** * Creates an instance of ObserverCall. * @param {ObserverName} name * @param {T} [value] */ constructor( /** * The name of the intercepted observer call. * * @type {ObserverName} **/ name, /** * (Optional) The value, if any, emitted by the observable. * * @type {T} * @template T The type of value emitted by the observable. */ value) { this.name = name; this.value = value; } /** * Creates an ObserverCall representing the resolving state of an observable. * * @static * @template T The type of value emitted by the observable. * @return {ObserverCall<T>} */ static resolving() { return new ObserverCall('resolving'); } /** * Extracts the data from a materialized observable notification and creates an `ObserverCall` representation for it. * * @static * @template T The type of value emitted by the observable. * @param {Notification<T>} notification The notification received from the materialized observable. * @return {ObserverCall<T>} An `ObserverCall` representing the notification and its data. */ static fromNotification({ kind, value, error }) { return new ObserverCall(StateNotificationMap[kind], error || value); } } /** * Represents a state describing a view to be rendered or a view already rendered. The state holds the parameterization indicating * when a view should be rendered and destroyed, and also holds the rendered view if there is any. * * States are created every time an {@link ObserverCall} is emitted. They are used by {@link OnObserverBaseDirective `*onObserver`} directives * to understand how a view should be rendered and initiate a commitment to render flow. * * The state is immutable. * * @export * @class ViewRenderState * @template T The type of value emitted by the observable. */ class ViewRenderCommitment { /** * Creates an instance of ViewRenderState. */ constructor( /** The id of the commitment to render. Allows identifying the state within a state map. */ commitmentId, /** The intercepted call which triggered the commitment to render. */ call, /** The duration (in milliseconds) specified as the delay before rendering the view. */ showAfter, /** The duration (in milliseconds) specified as the delay before destroying the view. */ showFor, /** * The timestamp at which the view should be rendered. This value is manually specified and not calculated automatically using * `Date.now()` upon state creation because states might be recreated before the {@link showAfter} delay is finished. * When a state is recreated, the time that has already passed should be considered, thus the previous value should be used. */ renderAt, /** (Optional) The rendered view. Will be provided only after the recreation of the state once the delay has passed. */ view) { this.commitmentId = commitmentId; this.call = call; this.showAfter = showAfter; this.showFor = showFor; this.renderAt = renderAt; this.view = view; } /** * The timestamp at which the view should be destroyed. * * @readonly * @type {number} */ get destroyAt() { return this.showFor ? this.renderAt + this.showFor : undefined; } /** * `true` if the state represents a view that is currently rendered; otherwise `false`. * * @readonly * @type {boolean} */ get isRendered() { return !!this.view; } /** * `true` if the state represents a view that should be auto-destroyed; otherwise `false`. * * @readonly * @type {boolean} */ get autoDestroys() { return !!this.destroyAt; } /** * Creates a new state representing a new, fresh, commitment to render. * Should be used in multi-view mode, or in single-view mode when there is nothing rendered. * * @static * @template T The type of value emitted by the observable. * @param {ObserverCall<T>} call The intercepted call which triggered this state. * @param {number} showAfter The duration (in milliseconds) to wait before rendering the view. * @param {number} showFor The duration (in milliseconds) to wait before destroying the view. * @return {ViewRenderCommitment<T>} A new state representing fresh commitment to render. */ static create(call, showAfter, showFor) { const now = Date.now(); return new ViewRenderCommitment(now.toString(), call, showAfter, showFor, now + showAfter); } /** * Clones the state and replaces the call which triggered it. * Should be used in single-view mode when the view is already rendered and a new call is intercepted to make sure the * latest emitted value is specified. * * @static * @template T The type of value emitted by the observable. * @param {ViewRenderCommitment<T>} state The state to clone. * @param {ObserverCall<T>} call The intercepted call which triggered this state. * @return {ViewRenderCommitment<T>} A new state representing an updated commitment to render. */ static update(state, call) { const now = Date.now(); // In single-view mode, in case the view is already rendered and a new call is intercepted, the latest emitted value should // reset renderAt date so the user has a chance to see the latest value. return new ViewRenderCommitment(state.commitmentId, call, state.showAfter, state.showFor, now + state.showAfter, state.view); } /** * Clones the state and assigns it with a recently rendered view. * Should be used whenever a view is rendered. * * @static * @template T The type of value emitted by the observable. * @param {ViewRenderCommitment<T>} state The state to clone. * @param {RenderedView<T>} view The rendered view to store in the state. * @return {ViewRenderCommitment<T>} A new state with the rendered view. */ static rendered(state, view) { return new ViewRenderCommitment(state.commitmentId, state.call, state.showAfter, state.showFor, state.renderAt, view); } } /** * The default number of times the countdown will be updated in a rendered view waiting to be auto-destroyed. * To change this, the user will have to specify a value for the {@link OnObserverBaseDirective.countdownInterval `countdownInterval`} property. **/ const DefaultCountdownUpdateCount = 30; /** * Provides functionality for `*onObserver<state>` directives that render templates according to the state of an observable. * * Any template assigned with the directive will render when the defined observer calls are intercepted, and destroyed when any other calls are * intercepted. For example, if the directive intercepts `next` calls, the view will render on the first value emission, then destroy on * `complete` or `error`. * * ## Features * * #### View Context * Use the microsyntax `as` keyword to assign resolved values to a variable. * Use the microsyntax `let` keyword to assign the {@link OnObserverContext full context object} to a variable (e.g. `let context`). * * #### Delayed rendering * Specify a value for {@link OnObserverBaseDirective.showAfter `showAfter`} to delay rendering. * * #### Auto destroy * Specify {@link OnObserverBaseDirective.showFor `showFor`} to automatically destroy the view after a certain duration. * * #### Countdown updates * When {@link OnObserverBaseDirective.showFor `showFor`} is specified, the view context will be updated with the time remaining until the view * is destroyed and the time elapsed since it was rendered. This allows giving the user feedback in a progress bar, a spinner, a textual timer * or any other UI component. * * Remaining is provided by the {@link OnObserverContext.remaining `remaining`} property. Elapsed time is provided by the {@link OnObserverContext.elapsed `elapsed`} * property. Access it by assigning a variable using `let`, like so: * `let remaining = remaining` * * #### Multi view mode * Specify {@link OnObserverBaseDirective.viewMode `viewMode = 'multiple'`} to enable rendering a new view for each intercepted call * instead of updating a single rendered view. This allows stacking logs, notification snackbars, or any other aggregation functionality. * Combined with {@link OnObserverBaseDirective.showFor `showFor`}, this is great for disappearing messages/notifications. * * #### View index * In multi-view mode, the context will contain the index of the view, which can be used for calculations and styling. * * #### Multi call interception * Create different interception combinations by specifying more than one call name in {@link OnObserverBaseDirective.renderOnCallsTo `renderOnCallsTo`}. * This allows, for example, the combination of `'error'` and `'complete'` to create a directive named `*onObserverFinalized`. * * ## Extending * As this base class doesn't know what the properties of the extending class will be, extending classes must: * 1. Define their selector in the abstract {@link OnObserverBaseDirective.selector `selector`} property. This will allow the directive to * assign the view context with a property which will enable the microsyntax `as` keyword. * 2. Define the call(s) to intercept and render the view for in the abstract {@link OnObserverBaseDirective.renderOnCallsTo `renderOnCallsTo`}. * 3. Define an `@Input() set <selector>` property which will call `this.input.next(value)`. * 4. Define an `@Input() set <selector>ViewMode` property which will set `this.viewMode`. * 5. Define an `@Input() set <selector>ShowAfter` property which will set `this.showAfter`. * 6. Define an `@Input() set <selector>ShowFor` property which will set `this.showFor`. * 7. Define an `@Input() set <selector>CountdownInterval` property which will set `this.countdownInterval`. * 8. Define this static context type guard to allow strong typing the template: * ```ts * static ngTemplateContextGuard<T>(directive: ___DIRECTIVE_NAME___<T>, context: unknown): context is OnObserverContext<T> * { return true; } * ``` * * @export * @abstract * @class OnObserverBaseDirective * @extends {Destroyable} * @implements {OnInit} * @template T The type of value the observable will emit. */ class OnObserverBaseDirective extends Destroyable { constructor(template, viewContainer) { super(); this.template = template; this.viewContainer = viewContainer; /** * A global commitment map holding all commitments to render for which the directive has created commitment observables. * Ids are the timestamp of the observed calls and values are the commitments with their rendering parameters. * * @private * @type {RenderCommitmentMap<T>} */ this.commitments = new Map(); /** * (Optional) The view mode the directive will ope