UNPKG

@bespunky/angular-zen

Version:

The Angular tools you always wished were there.

391 lines (390 loc) 22.1 kB
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>; }