Home Manual Reference Source Repository

docs/view/View.js

import { VNode } from 'snabbdom/vnode';
import h from 'snabbdom/h';

import { Type, ProviderArg, Inject, Injector, Optional, forwardRef } from '../di';
import { Renderer, Renderable, ConfiguredRenderable } from '../dom';
import { ContainerRef, ConfigurationRef, ElementRef, DocumentRef, ContextType } from '../common';
import { Stack } from '../stack';
import { ViewContainer } from './ViewContainer';
import { ViewConfig, ResolverStrategy, CacheStrategy } from './common';
import { Subject, Observable, BeforeDestroyEvent, BehaviorSubject } from '../events';
import { MakeVisibleCommand, MinimizeCommand } from '../commands';
import { ViewManager } from './ViewManager';
import { ViewFactory } from './ViewFactory';
import { CustomViewHookEvent } from './CustomViewHookEvent';
import { get } from '../utils';
import { StateContext } from '../StateContext';
import { SizeChanges } from './hooks';

/**
 * A renderable that renders a component.
 * @export
 * @class View
 * @extends {Renderable}
 */
export class View extends Renderable {
  protected _viewContainer: ViewContainer<any>;
  protected _visibilityChanges: BehaviorSubject<boolean> = new BehaviorSubject(true);
  protected _sizeChanges: BehaviorSubject<SizeChanges> = new BehaviorSubject({ width: -1, height: -1 });
  protected _viewContainerCreated: Subject<ViewContainer<any>> = new Subject();
  protected _initialCreate: boolean = true;
  
  /**
   * Notifies when the visibility of this view changes.
   * @type {Observable<boolean>}
   */
  visibilityChanges: Observable<boolean> = this._visibilityChanges.asObservable().distinctUntilChanged();
  /**
   * Notifies when the dimensions of this view changes.
   * @type {Observable<{ width: number, height: number }>}
   */
  sizeChanges: Observable<SizeChanges> = this._sizeChanges.asObservable();

  /**
   * Notifies when the view container is resolved.
   * @type {Observable<ViewContainer<any>>}
   */
  viewContainerCreated: Observable<ViewContainer<any>> = this._viewContainerCreated.asObservable();
  
  /**
   * Creates an instance of View.
   * @param {Renderable} _container 
   * @param {ViewConfig} _configuration 
   * @param {ViewManager} _viewManager 
   * @param {ViewFactory} _viewFactory 
   * @param {Document} _document 
   */
  constructor(
    @Inject(ContainerRef) protected _container: Renderable,
    @Inject(ConfigurationRef) protected _configuration: ViewConfig,
    @Inject(ViewManager) protected _viewManager: ViewManager,
    @Inject(ViewFactory) protected _viewFactory: ViewFactory,
    @Inject(DocumentRef) protected _document: Document,
    @Inject(StateContext) protected _stateContext: StateContext
  ) {
    super();
  }

  get width(): number {
    return this._container.width;
  }

  get height(): number {
    return this._container.height;
  }

  /**
   * Whether this view is configured to be lazy.
   * @readonly
   * @type {(boolean|null)}
   */
  get lazy(): boolean|null {
    return this.resolveConfigProperty<boolean>('lazy');
  }
  
  /**
   * Whether this view in configured to be cacheable.
   * @readonly
   * @type {(boolean|null)}
   */
  get isCacheable(): boolean {
    const cacheStrategy = this.caching;

    if ((cacheStrategy === CacheStrategy.RELOAD && this._stateContext.context === ContextType.NONE) 
      || cacheStrategy === CacheStrategy.PERSISTENT) {
      return true;
    }

    return false;
  }

  get caching(): CacheStrategy|null {
    return this.resolveConfigProperty<CacheStrategy>('caching');
  }
  
  /**
   * The 'ref' string this view is configured with.
   * @readonly
   * @type {(string|null)}
   */
  get ref(): string|null {
    return this.resolveConfigProperty<string>('ref');
  }
  
  /**
   * The resolution strategy this view is configured with.
   * @readonly
   * @type {(ResolverStrategy|null)}
   */
  get resolution(): ResolverStrategy|null {
    return this.resolveConfigProperty<ResolverStrategy>('resolution');
  }
  
  /**
   * The token this view is using for registration.
   * @readonly
   * @type {(any|null)}
   */
  get token(): any|null {
    return this._viewFactory.getTokenFrom(this._configuration);
  }

  initialize(): void {
    super.initialize();
    
    this._renderer.rendered
      .takeUntil(this.destroyed)
      .subscribe(this._postRender.bind(this));
  }

  render(): VNode {
    return h('div.ug-layout__view-container', {
      key: this.uid,
      style: {
        width: `${this.width}px`,
        height: `${this.height}px`
      },
      hook: {
        create: (oldNode, newNode) => this._onCreate(newNode.elm as HTMLElement)
      }
    });
  }

  destroy(): void {
    this._sizeChanges.complete();
    this._visibilityChanges.complete();

    super.destroy();
  }

  /**
   * Closes this view.
   * @emits {BeforeDestroyEvent} Fired when not silent.
   * @param {{ silent?: boolean }} [args={}] 
   */
  close(args: { silent?: boolean } = {}): void {
    const { silent = false } = args;
    
    if (!silent) {
      const event = new BeforeDestroyEvent(this);
      
      this._eventBus.next(event);
      event.results().subscribe(() => this.remove());
    } else {
      this.destroy();
    }
  }

  /**
   * Makes this view visible.
   * @emits {MakeVisibileCommand}
   */
  makeVisible(): void {
    this.emitUp(new MakeVisibleCommand(this));
  }

  /**
   * Minimizes this view if applicable.
   * @emits {MinimizeCommand}
   */
  minimize(): void {
    this.emitUp(new MinimizeCommand(this));
  }

  /**
   * Resolves a views config property. Checks the configuration given to the
   * view renderable first then checks the component metadata.
   * @template T The return type.
   * @param {string} path 
   * @returns {(T|null)} 
   */
  resolveConfigProperty<T>(path: string): T|null {
    return this._viewFactory.resolveConfigProperty<T>(this._configuration, path);
  }

  /**
   * Invoked on every render cycle.
   * @private
   */
  private _postRender(): void {
    // Check for these changes every render iteration.
    this._visibilityChanges.next(this.isVisible());
    this._sizeChanges.next({ width: this.width, height: this.height });
  }

  /**
   * Invoked when snabbdom has created the HTML element for this view.
   * @private
   * @param {HTMLElement} element 
   */
  private _onCreate(element: HTMLElement): void {
    if (!this._viewContainer) {
      let container = this._viewManager.resolve<any>(this._configuration);

      if (container) {
        if (container.hasComponent) {
          container.resolve({ fromCache: true });
        }
      } else {
        container = this._viewManager.create({
          config: this._configuration,
          injector: this.injector
        });
      }

      this._viewContainer = container;
      this._viewContainerCreated.next(this._viewContainer);
    }

    this._viewContainer.setView(this);
    this._viewContainer.mountTo(element);
  }

  /**
   * Configures a view.
   * @static
   * @param {ViewConfig} config 
   * @returns {ConfiguredRenderable<View>} 
   */
  static configure(config: ViewConfig): ConfiguredRenderable<View> {
    return new ConfiguredRenderable(View, config);
  }
}