Home Manual Reference Source Repository

docs/view/ViewManager.js

import { Inject, Type } from '../di';
import { ViewFactory, ViewFactoryArgs } from './ViewFactory';
import { ViewContainer } from './ViewContainer';
import { Subject, Observable, Observer } from '../events';
import {
  VIEW_CONFIG_KEY, 
  ViewConfig, 
  ViewComponentConfig, 
  ResolverStrategy,
  ViewQueryArgs
} from './common';
import { propEq } from '../utils';

export interface ViewManagerEvent<T> {
  ref?: string|null;
  token: any;
  container: ViewContainer<T>;
}

export interface RefChangeEvent<T> {
  ref: string;
  container: ViewContainer<T>;
  token: any;
  type: 'add'|'remove';
}

export interface ViewResolutionOptions {
  emit?: boolean;
}

export class ViewManager {
  private _views: Map<any, { [key: string]: ViewContainer<any> }> = new Map();
  private _refs: Map<string, ViewContainer<any>> = new Map();
  private _resolved: Subject<ViewManagerEvent<any>> = new Subject();
  private _created: Subject<ViewManagerEvent<any>> = new Subject();
  private _destroyed: Subject<void> = new Subject<void>();
  private _refChanges: Subject<RefChangeEvent<any>> = new Subject();
  private _viewInit: Subject<ViewManagerEvent<any>> = new Subject();

  resolved: Observable<ViewManagerEvent<any>> = this._resolved.asObservable();
  created: Observable<ViewManagerEvent<any>> = this._created.asObservable();
  destroyed: Observable<void> = this._destroyed.asObservable();
  refChanges: Observable<RefChangeEvent<any>> = this._refChanges.asObservable();
  viewInit: Observable<ViewManagerEvent<any>> = this._viewInit.asObservable();
  
  constructor(
    @Inject(ViewFactory) private _viewFactory: ViewFactory
  ) {}

  has(token: any, id?: number): boolean {
    const map = this.getAll(token);
    
    if (!map) {
      return false;
    }

    if (id != null) {
      return map.hasOwnProperty(id);
    }

    return true;
  }

  get<T>(token: any, id: number): ViewContainer<T>|null {
    const map = this.getAll(token);

    if (!map) {
      return null;
    }

    return map.hasOwnProperty(id) ? map[id] : null;
  }

  getAll(token: any): ({ [key:number]: ViewContainer<any> })|null {
    return this._views.get(token) || null;
  }

  destroy() {
    for (const views of this._views.values()) {
      for (const key of Object.keys(views)) {
        const view = views[key] as ViewContainer<any>|null;

        if (view) {
          view.destroy();  
        }
      }
    }      

    this._destroyed.next();
    this._destroyed.complete();
    this._created.complete();
    this._resolved.complete();
    this._refChanges.complete();
    this._viewInit.complete();
  }

  getRef<T>(ref: string): ViewContainer<T>|null {
    return this._refs.get(ref) || null;
  }

  resolve<T>(config: ViewConfig, options: ViewResolutionOptions = {}): ViewContainer<T>|null {
    const token = this._viewFactory.getTokenFrom(config);
    const metadata = this._assertAndReadComponent(token);

    return this._resolve<T>(config, metadata, options);
  }
  
  resolveOrCreate<T>(args: ViewFactoryArgs, options: ViewResolutionOptions = {}): ViewContainer<T> {
    return this.resolveOrCreateWith<T>(args.config, (): ViewContainer<T> => {
      return this._viewFactory.create<T>(args);
    }, options);
  }
  
  resolveOrCreateWith<T>(config: ViewConfig, factory: (viewFactory: ViewFactory) => ViewContainer<T>, options: ViewResolutionOptions = {}): ViewContainer<T> {
    const token = this._viewFactory.getTokenFrom(config);
    const metadata = this._assertAndReadComponent(token);

    const container = this._resolve<T>(config, metadata, options);

    if (container) {
      return container;
    }

    return this.createWith<T>(config, factory, options);
  }

  create<T>(args: ViewFactoryArgs, options: ViewResolutionOptions = {}): ViewContainer<T> {
    return this.createWith(args.config, (): ViewContainer<T> => {
      return this._viewFactory.create<T>(args);
    }, options);
  }
  
  createWith<T>(config: ViewConfig, factory: (viewFactory: ViewFactory) => ViewContainer<T>, options: ViewResolutionOptions = {}): ViewContainer<T> {
    const token = this._viewFactory.getTokenFrom(config);
    
    this._assertAndReadComponent(token);
    
    const container = factory(this._viewFactory);

    this._created.next({
      container,
      token,
      ref: config.ref
    });
    
    this.register(token, container, { ref: config.ref });
    
    return container;
  }

  register(token: any, container: ViewContainer<any>, options: { ref?: string|null } = {}): void {
    const { ref } = options;
    
    if (!this._views.has(token)) {
      this._views.set(token, {});
    }

    const map = this._views.get(token) as { [key:number]: ViewContainer<any> };

    if (map.hasOwnProperty(container.id)) {
      throw new Error(`An view instance entry already exists for ${token} ${container.id}`);
    }

    map[container.id] = container;

    this._views.set(token, map);

    container.destroyed.subscribe(() => this.unregister(token, container, { ref }));
    
    if (ref) {
      if (this._refs.has(ref)) {
        throw new Error(`Ref '${ref}' already exists.`);
      }
      
      this._refs.set(ref, container);
      this._refChanges.next({ ref, container, token, type: 'add' });
    }
  }
  
  unregister(token: any, container: ViewContainer<any>, options: { ref?: string|null } = {}): void {
    const { ref } = options;
    const map = this._views.get(token) as { [key:number]: ViewContainer<any> };

    if (!map) {
      throw new Error(`No entries exist for token ${token}`);
    }

    if (!map.hasOwnProperty(container.id)) {
      throw new Error(`An view instance entry already exists for ${token} ${container.id}`);
    }

    delete map[container.id];

    if (Object.keys(map).length === 0) {
      this._views.delete(token);
    }

    if (ref && this._refs.has(ref)) {
      this._refs.delete(ref);
      this._refChanges.next({ ref, container, token, type: 'remove' });
    }
  }

  private _resolve<T>(config: ViewConfig, metadata: ViewComponentConfig, options: ViewResolutionOptions): ViewContainer<T>|null {
    const resolution = this._viewFactory.resolveConfigProperty(config, 'resolution');
    let result: ViewContainer<T>|null = null;
      
    // REF
    if (resolution === ResolverStrategy.REF) {
      if (config.ref && this._refs.has(config.ref)) {
        result = this.getRef(config.ref) as ViewContainer<T>;
      }
    } else {
      // SINGLETON
      const token = this._viewFactory.getTokenFrom(config);
      const views = this.getAll(token);

      if (views) {
        const keys = Object.keys(views);

        if (keys.length) {
          result = views[keys[0]];
        }
      }
    }

    if (result && options.emit !== false) {
      this._resolved.next({
        ref: config.ref,
        container: result,
        token: this._viewFactory.getTokenFrom(config)
      });
    }
    
    return result;
  }

  /**
   * Notifies when the a view is registered matching the given query.
   * @template T 
   * @param {{ token?: any, ref?: string }} [query={}] 
   * @returns {Observable<ViewContainer<T>>} 
   */
  subscribeToQuery<T>(query: ViewQueryArgs = {}): Observable<ViewContainer<T>> {
    const { ref, token, id } = query;
  
    if (ref) {
      return this.queryRef(ref);
    } else if (token) {
      return this.queryToken(token, id);
    }

    return Observable.empty();
  }

  queryToken<T>(token: any, id?: number): Observable<ViewContainer<T>> {
    return Observable.create((observer: Observer<ViewContainer<T>>) => {
      for (const container of this.query<T>({ token, id })) {
        observer.next(container);
      }

      return this.viewInit
        .filter(propEq('token', token))
        .filter(e => id ? id === e.container.id : true)
        .map(e => e.container)
        .subscribe(observer);
    });
  }

  queryRef<T>(ref: string): Observable<ViewContainer<T>> {
    return Observable.create((observer: Observer<ViewContainer<T>>) => {
      const result = this.query<T>({ ref });
      
      if (result.length) {
        observer.next(result[0]);
      }

      return this.refChanges
        .filter(propEq('type', 'add'))
        .filter(propEq('ref', ref))
        .map(e => e.container)
        .subscribe(observer);
    });
  }

  query<T>(query: ViewQueryArgs): ViewContainer<T>[] {
    const { ref, id, token } = query;
    
    if (ref && this._refs.has(ref)) {
      return [ this._refs.get(ref) as ViewContainer<T> ];
    } else if (token) {
      if (this.has(token)) {
        const containers = this.getAll(token) as { [key: number]: ViewContainer<T> };

        if (!id) {
          return Object.keys(containers).map(id => containers[id]);
        }
        
        for (const containerId of Object.keys(containers)) {
          if (id && containerId === id.toString()) {
            return [ containers[containerId] ];
          }
        }
      }
    }

    return [];
  }

  private _assertAndReadComponent(token: any): ViewComponentConfig {
    const metadata = Reflect.getOwnMetadata(VIEW_CONFIG_KEY, token) as ViewComponentConfig|undefined;

    if (!metadata) {
      throw new Error(`The given token is not a registered ViewComponent.`);
    }

    return metadata;
  }
}