@bespunky/angular-zen
Version:
The Angular tools you always wished were there.
391 lines (390 loc) • 22.1 kB
TypeScript
import { Observable, BehaviorSubject } from 'rxjs';
import { OnInit, TemplateRef, ViewContainerRef } from '@angular/core';
import { Destroyable } from '../../destroyable/destroyable';
import { ObserverName, DurationAnnotation, ViewMode } from '../abstraction/types/general';
import { OnObserverContext } from './types/on-observer-context';
import * as i0 from "@angular/core";
/**
* 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.
*/
export declare abstract class OnObserverBaseDirective<T> extends Destroyable implements OnInit {
private readonly template;
private readonly 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>}
*/
private commitments;
/**
* The first commitment in the {@link OnObserverBaseDirective.commitments global commitments map}. Used when working with a single view
* to retrieve its corresponding single commitment.
*
* @readonly
* @private
* @type {ViewRenderCommitment<T> | undefined}
*/
private get mainCommitment();
/**
* The selector defined for the directive extending this class. Will be used to create a corresponding
* property in the view context in order to make the micro-syntax `as` keyword work.
*
* @protected
* @abstract
* @type {string}
*/
protected abstract readonly selector: string;
/**
* The observer name(s) for which to intercept calls.
*
* @protected
* @abstract
* @type {(ObserverName | ObserverName[])}
*/
protected abstract renderOnCallsTo: ObserverName | ObserverName[];
/**
* (Optional) The view mode the directive will operate in:
* `'single'` - A single view will be rendered on intercepted calls. If a view has already been rendered when a call is intercepted,
* the existing view will be updated with data from the new call.
*
* `'multiple'` - Every new intercepted call will render a new view with its own context and data encapsulated from the current call.
*
* Default is `'single'`.
*
* ⚠️ Extending classes should:
* 1. Declare an `@Input()` setter named `{selector}ViewMode` (e.g. onObserverCompleteViewMode) which will set this value.
* 2. Provide the above documentation for the setter property.
*
* @default 'single'
* @protected
* @type {ViewMode}
*/
protected viewMode: ViewMode;
/**
* (Optional) The duration for which the directive should wait before rendering the view once an intercepted call is made.
*
* You can specify a number, which will be treated as milliseconds, or a string with the format of `<number><ms | s | ms>`.
* Numbers can be either integers or floats.
* For example:
* - `3000` - Wait for 3 seconds, then render the view.
* - `'10s'` - Wait for 10 seconds, then render the view.
* - `'0.5m'` - Wait for 30 seconds, then render the view.
* - `'100ms'` - Wait for 100 milliseconds, then render the view.
*
* Default is `0`, meaning immediately render the view.
*
* TODO: ADD LINK TO TOUR OR FULL WIKI PAGE
* Read more {@link OnObserverBaseDirective About render flow}.
*
* ⚠️ Extending classes should:
* 1. Declare an `@Input()` setter named `{selector}ShowAfter` (e.g. onObserverCompleteShowAfter) which will set this value.
* 2. Provide the above documentation for the setter property.
*
* @protected
* @type {DurationAnnotation}
*/
protected showAfter: DurationAnnotation;
/**
* (Optional) The duration for which the view should be rendered. When the duration passes, the view will be auto destroyed.
*
* You can specify a number, which will be treated as milliseconds, or a string with the format of `<number><ms | s | ms>`.
* Numbers can be either integers or floats.
* For example:
* - `3000` - The view will be destroyed after 3 seconds.
* - `'10s'` - The view will be destroyed after 10 seconds.
* - `'0.5m'` - The view will be destroyed after 30 seconds.
* - `'100ms'` - The view will be destroyed after 100 milliseconds.
*
* During the time the view is rendered, the context will be updated with a countdown object to facilitate any UI part used to
* indicate countdown to the user. The countdown will be exposed through the {@link OnObserverContext.remaining `remaining`}
* property and the elapsed time through {@link OnObserverContext.elapsed `elapsed`} property in the view context and can both
* be accessed be declaring a `let` variable (e.g. `let remaining = remaining`).
* See {@link OnObserverBaseDirective.countdownInterval `countdownInterval`} for changing the updates interval.
*
* When unspecified, the view will be destroyed immediately once the observer detects a call different to the intercepted ones.
*
* TODO: ADD LINK TO TOUR OR FULL WIKI PAGE
* Read more {@link OnObserverBaseDirective About render flow}.
*
* ⚠️ Extending classes should:
* 1. Declare an `@Input()` setter named `{selector}ShowFor` (e.g. onObserverCompleteShowFor) which will set this value.
* 2. Provide the above documentation for the setter property.
*
* @protected
* @type {DurationAnnotation}
*/
protected showFor?: DurationAnnotation;
/**
* ### Only used when passing a value to {@link OnObserverBaseDirective.showFor `showFor`}.
*
* (Optional) The interval with which countdown updates should be made to the view's context before it auto destroys.
* The lower the value, the more updates will be made to the context, but the more resources your directive will consume.
*
* You can specify a number, which will be treated as milliseconds, or a string with the format of `<number><ms | s | ms>`.
* Numbers can be either integers or floats.
* For example:
* - `3000` - 3 seconds between each update.
* - `'10s'` - 10 seconds between each update.
* - `'0.5m'` - 30 seconds between each update.
* - `'100ms'` - 100 milliseconds between each update.
*
* You can also specify `'animationFrames'` so the countdown gets updated each time the browser is working on animations.
*
* When unspecified, the total duration of the countdown will be divided by {@link DefaultCountdownUpdateCount `DefaultCountdownUpdateCount`}
* to get a fixed interval which will make for {@link DefaultCountdownUpdateCount `DefaultCountdownUpdateCount`} countdown updates.
*
* ⚠️ Extending classes should:
* 1. Declare an `@Input()` setter named `{selector}CountdownInterval` (e.g. onObserverCompleteCountdownInterval) which will set this value.
* 2. Provide the above documentation for the setter property.
*
* @protected
* @type {DurationAnnotation}
*/
protected countdownInterval?: DurationAnnotation | 'animationFrames';
/**
* ### Why BehaviorSubject<... | null> and not Subject<...>
* `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.
*/
protected readonly input: BehaviorSubject<Observable<T> | null>;
/**
* `true` if {@link OnObserverBaseDirective.viewMode viewMode} is `'single'`; otherwise, `false`.
*
* @readonly
* @type {boolean}
*/
get isSingleView(): boolean;
/**
* `true` if {@link OnObserverBaseDirective.viewMode viewMode} is `'multiple'`; otherwise, `false`.
*
* @readonly
* @type {boolean}
*/
get isMultiView(): boolean;
constructor(template: TemplateRef<OnObserverContext<T>>, viewContainer: ViewContainerRef);
ngOnInit(): void;
/**
* Destroys any rendered view.
*
* @private
*/
private destroyAll;
/**
* Creates the main feed the directive will subscribe to. The feed will listen to changed to {@link OnObserverBaseDirective.input `input`},
* then switch to the newly received observable in order to start observing it.
* The newly received observable will then be materialized and calls will be aggregated as commitment objects with information about
* what to render and when. Those commitments will pass through the {@link OnObserverBaseDirective.onCommitmentsChanged onCommitmentsChanged()} method
* which will update the global commitment and create observables with commitments to render and auto destroy views according to the
* given commitments.
*
* This feed is the single reactive entrypoint, meaning any observable created by the directive will be created inside of this
* observable or its nested observables. Any time a nested observable is created it will be switched to. This allows the pipeline to
* completely startover when a new call is made or a new {@link OnObserverBaseDirective.input `input`} observable is provided, thus keeping
* a consistent stream of data to the {@link OnObserverBaseDirective.commitments global commitments map}.
*
* @private
* @return {Observable<ViewRenderCommitment<T>[]>} An observable as described above.
*/
private renderFeed;
/**
* Materializes the observable and converts notifications to an {@link ObserverCall} object.
* The returned observable will always start with a `'resolving'` call.
*
* @private
* @param {Observable<T>} input The observable to watch.
* @return {Observable<ObserverCall<T>>} A materialized observable which describes each observable notification as an {@link ObserverCall} object.
*/
private observeInput;
/**
* Checks whether the given observer call should be rendered according to the interception config in {@link OnObserverBaseDirective.renderOnCallsTo renderOnCallsTo}.
*
* @private
* @param {ObserverCall<T>} The call to check.
* @return {boolean} `true` if the call should be rendered; otherwise `false`.
*/
private shouldRender;
/**
* Creates the new commitments map when a new commitment should render.
*
* When `viewMode` is `'single'` the map will always contain a single commitment. If the commitment hasn't been rendered yet, a new commitment will be created.
* Otherwise, the existing commitment will be replaced by a clone with updated parameters (i.e. delay and countdown).
*
* When `viewMode` is `'multiple'` a new commitment will always be added to the map.
*
* @private
* @param {ObserverCall<T>} call The new call which should render.
* @return {RenderCommitmentMap<T>} The new map of commitments to render.
*/
private aggregateCommitments;
/**
* Creates the new commitments map when a new commitment shouldn't render.
*
* @private
* @return {RenderCommitmentMap<T>} If `showFor` is specified, meaning views should be auto destroyed after a certain duration,
* the current commitments will kept alive by returning them as a new map. This will allow recommiting to the same render parameters (i.e. delay and countdown).
* Otherwise, when views should destroy immediately, an empty map will be returned.
*/
private deaggregateCommitments;
/**
* Handles the changes to the current commitment of the watched observable and creates and commits to render all commitments.
*
* This will update the global commitment map. If an empty map is passed, all previous commitments will be destroyed.
*
* @private
* @param {RenderCommitmentMap<T>} commitments The current commitment map.
* @return {Observable<ViewRenderCommitment<T>[]>} An observable joining all render commitments.
*/
private onCommitmentsChanged;
/**
* Creates an observable that initiates the render flow for an emission. Render flow is as follows:
* 1. Delay render until the time for render (i.e. {@link ViewRenderCommitment.renderAt}) has come.
* 2. Render the view.
* 3. Update the {@link OnObserverBaseDirective.commitments global commitments map} with the rendered commitment.
* 4. Initiate an auto destroy timer. See {@link OnObserverBaseDirective.autoDestroy autoDestroy()}.
* 5. Remove the destroyed commitment from the {@link OnObserverBaseDirective.commitments global commitments map}.
*
* @private
* @param {RenderCommitmentMap<T>} commitments The current commitment map holding all commitments to render.
* @param {string} commitmentId The id of the commitment to render.
* @param {number} index The index of the view to be rendered.
* @return {Observable<ViewRenderCommitment<T>>} An observable that initiates the render flow for an emission.
*/
private commitToRender;
/**
* Creates an observable which delays the pipeline until the time to render the view (i.e. {@link ViewRenderCommitment.renderAt}) comes.
*
* @private
* @param {ViewRenderCommitment<T>} commitment The commitment to delay.
* @return {Observable<ViewRenderCommitment<T>>} An observable which delays the pipeline until the time to render the view (i.e. {@link ViewRenderCommitment.renderAt}) comes.
*/
private delayRender;
/**
* Creates a new context for the given commitment and renders (or updates) the view.
*
* @private
* @param {ViewRenderCommitment<T>} commitment The commitment for which to create the context and render the view.
* @param {number} index The index of the view. If `viewMode` is `'single'` this should always be `0` as there is only one view.
* @return {Observable<ViewRenderCommitment<T>>} An observable which renderes the commitment, then emits a new updated commitment referencing the rendered view.
*/
private renderCommitment;
/**
* Creates an interval observable which counts down until the time to destroy the view is reached, then destroys the view.
* While the timer is running, the rendered view's context will be updated in fixed intervals with the time left before destruction.
*
* @see {@link OnObserverBaseDirective.defineCountdownInterval defineCountdownInterval()} for more about the fixed countdown interval.
*
* If {@link OnObserverBaseDirective.countdownInterval `countdownInterval`} is `'animationFrames'`, the rxjs `animationFrames()` function
* will be used instead of the interval.
*
* @private
* @param {ViewRenderCommitment<T>} commitment The rendered commitment for which to initiate auto destroy.
* @return {Observable<ViewRenderCommitment<T>>} A timer observable which counts down until the time to destroy the view is reached, then destroys the view, while
* updating the context with the time left for destruction. The observable will emit the commitment
*/
private autoDestroy;
/**
* Makes sure the specified commitment is rendered and its context is updated, then returns an updated commitment with the rendered (or updated) view.
* If the view has been previously rendered, its context will be updated. Otherwise, the view will be rendered for the first time.
*
* The new commitment will be used further down the pipeline to update the internal `commitments` map.
*
* @see {@link OnObserverBaseDirective.commitToRender `commitToRender()`}.
*
* @private
* @param {ViewRenderCommitment<T>} commitment The commitment for which the view should be rendered.countdown
* @param {OnObserverContext<T>} context The context object to feed into the view.
* @return {ViewRenderCommitment<T>} The new commitment containing the rendered (or updated) view.
*/
private renderOrUpdateView;
/**
* Breaks down the time left before the view is destroyed to its parts and updates the view context so that the user may present
* a countdown or any other UI component indicating when the view will be destroyed.
*
* @private
* @param {RenderedView<T>} view The view in which to update the countdown.
* @param {number} timeLeftMs The time left (in milliseconds) for the view before being destroyed.
* @param {number} timeElapsedMs The time elapsed (in milliseconds) from the moment the view was rendered.
*/
private updateViewContextCountdown;
/**
* Defines the interval (in milliseconds) with which countdown updates should be made to the view's context.
* If the user has defined a value through {@link OnObserverBaseDirective.countdownInterval `countdownInterval`}, that value will be used.
* If the user has defined `'animationFrames'` as the value for {@link OnObserverBaseDirective.countdownInterval `countdownInterval`}, this will return `'animationFrames'`.
* Otherwise, {@link OnObserverBaseDirective.showFor `showFor`} will be divided by a fixed number defined by {@link DefaultCountdownUpdateCount `DefaultCountdownUpdateCount`}, currently 30, meaning the user will get
* 30 countdown updates with fixed intervals between them before the view is destroyed.
*
* @private
* @return {number} The interval with which countdown updates should be made to the view's context.
*/
private defineCountdownInterval;
static ɵfac: i0.ɵɵFactoryDeclaration<OnObserverBaseDirective<any>, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<OnObserverBaseDirective<any>, never, never, {}, {}, never, never, false>;
}