@bespunky/angular-zen
Version:
The Angular tools you always wished were there.
1,069 lines (1,052 loc) • 142 kB
JavaScript
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