docs/view/ViewContainer.js
import { Injector, Type, Inject, Optional } from '../di';
import { Renderable } from '../dom';
import { ReplaySubject, Observable, Subject, BeforeDestroyEvent, BehaviorSubject } from '../events';
import { ContainerRef, ConfigurationRef, DocumentRef } from '../common';
import { View } from './View';
import { ViewComponentRef } from './common';
import { CustomViewHookEvent } from './CustomViewHookEvent';
import { ViewHookExecutor, ViewHookMetadata } from './hooks';
import { isFunction, get, uid, eq, isPromise, isObject, Deferred } from '../utils';
export enum ViewContainerStatus {
READY,
PENDING,
FAILED
}
export enum ViewContainerAttachedStatus {
ATTACHED,
DETACHED
}
export interface ViewContainerReadyOptions {
/**
* Whether to initiale the component.
* @type {boolean}
*/
init?: boolean;
/**
* Statuses that tell when the component is ready.
* @type {ViewContainerStatus[]}
*/
when?: ViewContainerStatus[];
/**
* When the view transitions to one of these statuses then it will stop listening for status
* updates and the observable will complete without the component being emitted as ready.
* @type {ViewContainerStatus[]}
*/
until?: ViewContainerStatus[];
}
/**
* A container that holds a 1 to 1 relationship with a component instance. This instance
* is the main API for a component to interact with the layout framework.
* @export
* @class ViewContainer
* @template T The component type.
*/
export class ViewContainer<T> {
/**
* This element is the mount point that views can mount to.
* The node created by the render method gets recreated when moved
* somewhere else in the tree. This element is constant.
* @private
* @type {HTMLElement}
*/
private _element: HTMLElement = this._document.createElement('div');
private _component: T|null = null;
private _container: View|null;
private _status: BehaviorSubject<ViewContainerStatus> = new BehaviorSubject(ViewContainerStatus.PENDING);
private _destroyed: Subject<ViewContainer<T>> = new Subject();
private _beforeDestroy: Subject<BeforeDestroyEvent<Renderable>> = new Subject();
private _containerChange: Subject<View|null> = new Subject<View|null>();
private _visibilityChanges: BehaviorSubject<boolean> = new BehaviorSubject(true);
private _sizeChanges: BehaviorSubject<{ width: number, height: number }> = new BehaviorSubject({ width: -1, height: -1 });
private _attached: Subject<boolean> = new Subject();
private _initialized: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
private _componentReady: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
private _componentInitialized: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
private _isInitialized: boolean = false;
private _retry: Function|null = null;
private _statusInternal: ViewContainerStatus;
/**
* A unique identifier for this instance.
* @type {number}
*/
readonly id: number = uid();
/**
* Notifies that the view is being destroyed. This action is cancellable or can be halted until
* an async action is complete.
* @see Cancellable
* @type {Observable<BeforeDestroyEvent<Renderable>>}
*/
beforeDestroy: Observable<BeforeDestroyEvent<Renderable>> = this._beforeDestroy.asObservable();
/**
* Notifies when the view is destroyed.
* @type {Observable<ViewContainer<T>>}
*/
destroyed: Observable<ViewContainer<T>> = this._destroyed.asObservable();
/**
* Notifies when the visibility of this view changes.
* @type {Observable<boolean>}
*/
visibilityChanges: Observable<boolean> = this._visibilityChanges.asObservable();
/**
* Notifies when the dimensions of this views container has changed.
* @type {Observable<{ width: number, height: number }>}
*/
sizeChanges: Observable<{ width: number, height: number }> = this._sizeChanges
.asObservable()
.filter(e => e.height !== -1 && e.width !== -1)
.distinctUntilChanged((p, c) => p.width === c.width && p.height === c.height)
/**
* Notifies when the status of this component changes.
* @type {Observable<ViewContainerStatus>}
*/
status: Observable<ViewContainerStatus> = this._status.asObservable();
/**
* Contains the state of ths views initialized state.
* @type {Observable<boolean>}
*/
initialized: Observable<boolean> = this._initialized.asObservable();
/**
* Notifies when this container has changed the {@link View} it is associated with.
* @type {(Observable<View|null>)}
*/
containerChange: Observable<View|null> = this._containerChange.asObservable();
/**
* Notifies when the view has become attached.
* @type {Observable<boolean>}
*/
attached: Observable<boolean> = this._attached.asObservable().filter(eq(true));
/**
* Notifies when the view has become detached.
* @type {Observable<boolean>}
*/
detached: Observable<boolean> = this._attached.asObservable().filter(eq(false));
/**
* Notifies when the component is assigned to the container and ready for access.
* @type {Observable<boolean>}
*/
componentReady: Observable<boolean> = this._componentReady.asObservable();
/**
* Notifies when the component is initialized.
* @type {Observable<boolean>}
*/
componentInitialized: Observable<boolean> = this._componentInitialized.asObservable();
/**
* Creates an instance of ViewContainer.
* @param {Document} _document
* @param {Injector} _injector
*/
constructor(
@Inject(DocumentRef) protected _document: Document,
@Inject(Injector) protected _injector: Injector,
@Inject(ViewHookExecutor) protected _viewHookExecutor: ViewHookExecutor
) {
this._element.classList.add('ug-layout__view-container-mount');
this.attached.subscribe(() => this._executeHook('ugOnAttach', this._container));
this.detached.subscribe(() => this._executeHook('ugOnDetach'));
this.sizeChanges.subscribe(dimensions => this._executeHook('ugOnSizeChange', dimensions));
this.visibilityChanges.subscribe(isVisible => this._executeHook('ugOnVisibilityChange', isVisible));
this.beforeDestroy.subscribe(event => this._executeHook('ugOnBeforeDestroy', event));
this.initialized.subscribe(v => this._isInitialized = v);
// Reset the retry function when the component is no longer failed.
this.status.filter(eq(ViewContainerStatus.READY)).subscribe(() => this._retry = null);
this.status.subscribe(s => this._statusInternal = s);
}
get hasComponent(): boolean {
return Boolean(this._component);
}
/**
* Current width in pixels.
* @readonly
* @type {number}
*/
get width(): number {
return get(this._container, 'width', 0);
}
/**
* Current height in pixels.
* @readonly
* @type {number}
*/
get height(): number {
return get(this._container, 'height', 0);
}
/**
* The component instance associated with this container.
* @readonly
* @type {T|null}
*/
get component(): T|null {
return this._component;
}
/**
* Whether the container is cacheable.
* @readonly
* @type {boolean}
*/
get isCacheable(): boolean {
return this._container ? this._container.isCacheable : false;
}
get isLazy(): boolean {
return this._container ? Boolean(this._container.lazy) : false;
}
/**
* The HTML element for this container.
* @readonly
* @type {HTMLElement}
*/
get element(): HTMLElement {
return this._element;
}
get view(): View|null {
return this._container;
}
/**
* Get's a token from this containers injector. Note, this should not be used to grab
* parent renderables or any item that can be changed.
* @template U The return type.
* @param {*} token
* @returns {(U|null)}
*/
get<U>(token: any): U|null {
return this._injector.get(token, null);
}
/**
* Destroys the container.
*/
destroy(): void {
this._destroyed.next();
this._destroyed.complete();
this._status.complete();
this._beforeDestroy.complete();
this._containerChange.complete();
this._visibilityChanges.complete();
this._sizeChanges.complete();
this._initialized.complete();
}
/**
* Waits for the component to be ready. If the component is not initialized
* it will be initialized. This is important when the view is lazy and hasn't
* been shown yet but we need to interact with the component.
* @async
* @param {{ init?: boolean }} [options={}]
* @returns {Promise<ViewContainer<T>>}
*/
ready(options: ViewContainerReadyOptions = {}): Observable<ViewContainer<T>> {
return Observable.create(observer => {
const {
init = true,
when = [ ViewContainerStatus.READY, ViewContainerStatus.FAILED ],
until = []
} = options;
if (!this._isInitialized && init !== false) {
this.initialize();
}
return this.status
.subscribe(status => {
if (when.indexOf(status) !== -1) {
observer.next(this);
observer.complete();
} else if (until.indexOf(status) !== -1) {
observer.complete();
}
});
});
}
/**
* Initializes the component. This creates the component.
*/
initialize(): void {
if (this._isInitialized) {
return;
}
const component = this._injector.get(ViewComponentRef);
if (isPromise<T>(component)) {
component.then(val => this._onComponentReady(val));
} else {
this._onComponentReady(component);
}
this._initialized.next(true);
}
/**
* Sets the containing {@link View} for this container.
* @param {(View|null)} container
* @returns {void}
*/
setView(container: View|null): void {
if (container === this._container) {
return;
}
this._container = container;
// This needs to fire before we wire up to the new container.
this._containerChange.next(container);
if (this._container) {
this._container.destroyed
.takeUntil(this.containerChange)
.subscribe(() => this._onViewDestroy());
this._container
.scope(BeforeDestroyEvent)
.takeUntil(this.containerChange)
.subscribe(e => this._beforeDestroy.next(e));
this._container.visibilityChanges
.takeUntil(this.containerChange)
.subscribe(e => this._visibilityChanges.next(e));
this._container.sizeChanges
.takeUntil(this.containerChange)
.subscribe(e => this._sizeChanges.next(e));
this._container
.scope(CustomViewHookEvent)
.takeUntil(this.containerChange)
.subscribe(e => this._onCustomViewHook(e));
}
this._attached.next(Boolean(this._container));
}
/**
* Gets this renderables parent or any parent that is
* an instance of the passed in constructor. If non is found
* then null is returned.
* @template T The constructor type.
* @param {Type<T>} [Ctor]
* @returns {(T|null)}
*/
getParent<U extends Renderable>(Ctor: Type<U>): U|null {
return this._container ? this._container.getParent(Ctor) : null;
}
/**
* Gets this renderables parents or any parents that are
* an instance of the passed in constructor.
* @template T The constructor type.
* @param {Type<T>} [Ctor]
* @returns {(T|null)}
*/
getParents<U extends Renderable>(Ctor: Type<U>): U[] {
return this._container ? this._container.getParents(Ctor) : [];
}
/**
* Signals up the tree to make this {@link View} visible.
*/
makeVisible(): void {
if (this._container) {
this._container.makeVisible();
}
}
/**
* Determines whether this renderable is visible.
* @returns {boolean}
*/
isVisible(): boolean {
return this._container ? this._container.isVisible() : false;
}
/**
* Closes this view.
* @param {{ silent?: boolean }} args
*/
close(args: { silent?: boolean }): void {
if (this._container) {
this._container.close(args);
}
}
/**
* Mounts this containers element to the given element.
* @param {HTMLElement} element
*/
mountTo(element: HTMLElement): void {
element.appendChild(this._element);
}
/**
* Mounts an element to this containers element.
* @param {HTMLElement} element
*/
mount(element: HTMLElement): void {
this._element.appendChild(element);
}
/**
* Sets all the content of this containers element to the given HTML string.
* @param {string} html
*/
mountHTML(html: string): void {
this._element.innerHTML = html;
}
/**
* Detaches this container from it's view.
*/
detach(): void {
this.setView(null);
}
fail(retry?: Function|null): void {
// Can be nulled with explicit null value
if (retry || retry === null) {
this._retry = retry;
}
this._status.next(ViewContainerStatus.FAILED);
}
retry(): void {
if (this._retry) {
this._retry();
}
}
async resolve(options: { fromCache?: boolean } = {}): Promise<void> {
const { fromCache = false } = options;
if (!this.hasComponent) {
throw new Error('Can not resolve container without component being ready.');
}
this._status.next(ViewContainerStatus.PENDING);
try {
await this._executeHook('ugOnResolve', { fromCache });
this._status.next(ViewContainerStatus.READY);
} catch(e) {
this.fail(() => this.resolve(options));
throw e;
}
}
/**
* Invoked when the view renderable is destroyed.
* @private
*/
private _onViewDestroy(): void {
// If this view is cacheable, we don't destroy it.
if (this.isCacheable) {
this.detach();
} else {
this.destroy();
}
}
/**
* Executes a hook on the component.
* @private
* @param {string} name
* @param {...any[]} args
* @returns {*}
*/
private _executeHook(name: string, arg?: any): any {
if (this._component) {
return this._viewHookExecutor.execute<T>(this._component, name, arg);
}
}
private _onCustomViewHook(event: CustomViewHookEvent<any>): void {
if (event.execute) {
event.execute(this, this._viewHookExecutor, event);
} else {
this._executeHook(event.name, event.arg);
}
}
/**
* Invoked when the component is resolved from the initialization.
* @private
* @async
* @param {T} component
* @returns {Promise<void>}
*/
private async _onComponentReady(component: T): Promise<void> {
this._component = component;
// Link up any hooks right when the component is instantiated.
this._viewHookExecutor.linkObservers(this, this._component.constructor.prototype);
this._componentReady.next(true);
if (this._statusInternal === ViewContainerStatus.FAILED) {
return;
}
await this.resolve();
this._executeHook('ugOnInit', this);
this._componentInitialized.next(true);
}
}