/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {ApplicationRef} from '../application_ref'; import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection'; import {Injector, THROW_IF_NOT_FOUND} from '../di/injector'; import {isPresent} from '../facade/lang'; import {WtfScopeFn, wtfCreateScope, wtfLeave} from '../profile/profile'; import {DirectRenderer, RenderComponentType, Renderer} from '../render/api'; import {AnimationViewContext} from './animation_view_context'; import {ComponentRef} from './component_factory'; import {DebugContext, StaticNodeDebugInfo} from './debug_context'; import {ElementInjector} from './element_injector'; import {ExpressionChangedAfterItHasBeenCheckedError, ViewDestroyedError, ViewWrappedError} from './errors'; import {ViewContainer} from './view_container'; import {ViewRef_} from './view_ref'; import {ViewType} from './view_type'; import {ViewUtils, addToArray} from './view_utils'; const _scope_check: WtfScopeFn = wtfCreateScope(`AppView#check(ascii id)`); /** * @experimental */ const EMPTY_CONTEXT = new Object(); const UNDEFINED = new Object(); /** * Cost of making objects: http://jsperf.com/instantiate-size-of-object * */ export abstract class AppView { ref: ViewRef_; lastRootNode: any; allNodes: any[]; disposables: Function[]; viewContainer: ViewContainer; // This will be set if a view is directly attached to an ApplicationRef // and not to a view container. appRef: ApplicationRef; numberOfChecks: number = 0; renderer: Renderer; private _hasExternalHostElement: boolean; private _hostInjector: Injector; private _hostProjectableNodes: any[][]; private _animationContext: AnimationViewContext; private _directRenderer: DirectRenderer; public context: T; constructor( public clazz: any, public componentType: RenderComponentType, public type: ViewType, public viewUtils: ViewUtils, public parentView: AppView, public parentIndex: number, public parentElement: any, public cdMode: ChangeDetectorStatus, public declaredViewContainer: ViewContainer = null) { this.ref = new ViewRef_(this, viewUtils.animationQueue); if (type === ViewType.COMPONENT || type === ViewType.HOST) { this.renderer = viewUtils.renderComponent(componentType); } else { this.renderer = parentView.renderer; } this._directRenderer = (this.renderer as any).directRenderer; } get animationContext(): AnimationViewContext { if (!this._animationContext) { this._animationContext = new AnimationViewContext(this.viewUtils.animationQueue); } return this._animationContext; } get destroyed(): boolean { return this.cdMode === ChangeDetectorStatus.Destroyed; } create(context: T) { this.context = context; return this.createInternal(null); } createHostView(rootSelectorOrNode: string|any, hostInjector: Injector, projectableNodes: any[][]): ComponentRef { this.context = EMPTY_CONTEXT; this._hasExternalHostElement = isPresent(rootSelectorOrNode); this._hostInjector = hostInjector; this._hostProjectableNodes = projectableNodes; return this.createInternal(rootSelectorOrNode); } /** * Overwritten by implementations. * Returns the ComponentRef for the host element for ViewType.HOST. */ createInternal(rootSelectorOrNode: string|any): ComponentRef { return null; } /** * Overwritten by implementations. */ createEmbeddedViewInternal(templateNodeIndex: number): AppView { return null; } init(lastRootNode: any, allNodes: any[], disposables: Function[]) { this.lastRootNode = lastRootNode; this.allNodes = allNodes; this.disposables = disposables; if (this.type === ViewType.COMPONENT) { this.dirtyParentQueriesInternal(); } } injectorGet(token: any, nodeIndex: number, notFoundValue: any = THROW_IF_NOT_FOUND): any { let result = UNDEFINED; let view: AppView = this; while (result === UNDEFINED) { if (isPresent(nodeIndex)) { result = view.injectorGetInternal(token, nodeIndex, UNDEFINED); } if (result === UNDEFINED && view.type === ViewType.HOST) { result = view._hostInjector.get(token, notFoundValue); } nodeIndex = view.parentIndex; view = view.parentView; } return result; } /** * Overwritten by implementations */ injectorGetInternal(token: any, nodeIndex: number, notFoundResult: any): any { return notFoundResult; } injector(nodeIndex: number): Injector { return new ElementInjector(this, nodeIndex); } detachAndDestroy() { if (this.viewContainer) { this.viewContainer.detachView(this.viewContainer.nestedViews.indexOf(this)); } else if (this.appRef) { this.appRef.detachView(this.ref); } else if (this._hasExternalHostElement) { this.detach(); } this.destroy(); } destroy() { if (this.cdMode === ChangeDetectorStatus.Destroyed) { return; } const hostElement = this.type === ViewType.COMPONENT ? this.parentElement : null; if (this.disposables) { for (let i = 0; i < this.disposables.length; i++) { this.disposables[i](); } } this.destroyInternal(); this.dirtyParentQueriesInternal(); if (this._animationContext) { this._animationContext.onAllActiveAnimationsDone( () => this.renderer.destroyView(hostElement, this.allNodes)); } else { this.renderer.destroyView(hostElement, this.allNodes); } this.cdMode = ChangeDetectorStatus.Destroyed; } /** * Overwritten by implementations */ destroyInternal(): void {} /** * Overwritten by implementations */ detachInternal(): void {} detach(): void { this.detachInternal(); if (this._animationContext) { this._animationContext.onAllActiveAnimationsDone(() => this._renderDetach()); } else { this._renderDetach(); } if (this.declaredViewContainer && this.declaredViewContainer !== this.viewContainer && this.declaredViewContainer.projectedViews) { const projectedViews = this.declaredViewContainer.projectedViews; const index = projectedViews.indexOf(this); // perf: pop is faster than splice! if (index >= projectedViews.length - 1) { projectedViews.pop(); } else { projectedViews.splice(index, 1); } } this.appRef = null; this.viewContainer = null; this.dirtyParentQueriesInternal(); } private _renderDetach() { if (this._directRenderer) { this.visitRootNodesInternal(this._directRenderer.remove, null); } else { this.renderer.detachView(this.flatRootNodes); } } attachToAppRef(appRef: ApplicationRef) { if (this.viewContainer) { throw new Error('This view is already attached to a ViewContainer!'); } this.appRef = appRef; this.dirtyParentQueriesInternal(); } attachAfter(viewContainer: ViewContainer, prevView: AppView) { if (this.appRef) { throw new Error('This view is already attached directly to the ApplicationRef!'); } this._renderAttach(viewContainer, prevView); this.viewContainer = viewContainer; if (this.declaredViewContainer && this.declaredViewContainer !== viewContainer) { if (!this.declaredViewContainer.projectedViews) { this.declaredViewContainer.projectedViews = []; } this.declaredViewContainer.projectedViews.push(this); } this.dirtyParentQueriesInternal(); } moveAfter(viewContainer: ViewContainer, prevView: AppView) { this._renderAttach(viewContainer, prevView); this.dirtyParentQueriesInternal(); } private _renderAttach(viewContainer: ViewContainer, prevView: AppView) { const prevNode = prevView ? prevView.lastRootNode : viewContainer.nativeElement; if (this._directRenderer) { const nextSibling = this._directRenderer.nextSibling(prevNode); if (nextSibling) { this.visitRootNodesInternal(this._directRenderer.insertBefore, nextSibling); } else { const parentElement = this._directRenderer.parentElement(prevNode); if (parentElement) { this.visitRootNodesInternal(this._directRenderer.appendChild, parentElement); } } } else { this.renderer.attachViewAfter(prevNode, this.flatRootNodes); } } get changeDetectorRef(): ChangeDetectorRef { return this.ref; } get flatRootNodes(): any[] { const nodes: any[] = []; this.visitRootNodesInternal(addToArray, nodes); return nodes; } projectNodes(parentElement: any, ngContentIndex: number) { if (this._directRenderer) { this.visitProjectedNodes(ngContentIndex, this._directRenderer.appendChild, parentElement); } else { const nodes: any[] = []; this.visitProjectedNodes(ngContentIndex, addToArray, nodes); this.renderer.projectNodes(parentElement, nodes); } } visitProjectedNodes(ngContentIndex: number, cb: (node: any, ctx: C) => void, c: C): void { switch (this.type) { case ViewType.EMBEDDED: this.parentView.visitProjectedNodes(ngContentIndex, cb, c); break; case ViewType.COMPONENT: if (this.parentView.type === ViewType.HOST) { const nodes = this.parentView._hostProjectableNodes[ngContentIndex] || []; for (let i = 0; i < nodes.length; i++) { cb(nodes[i], c); } } else { this.parentView.visitProjectableNodesInternal(this.parentIndex, ngContentIndex, cb, c); } break; } } /** * Overwritten by implementations */ visitRootNodesInternal(cb: (node: any, ctx: C) => void, c: C): void {} /** * Overwritten by implementations */ visitProjectableNodesInternal( nodeIndex: number, ngContentIndex: number, cb: (node: any, ctx: C) => void, c: C): void {} /** * Overwritten by implementations */ dirtyParentQueriesInternal(): void {} internalDetectChanges(throwOnChange: boolean): void { if (this.cdMode !== ChangeDetectorStatus.Detached) { this.detectChanges(throwOnChange); } } detectChanges(throwOnChange: boolean): void { const s = _scope_check(this.clazz); if (this.cdMode === ChangeDetectorStatus.Checked || this.cdMode === ChangeDetectorStatus.Errored) return; if (this.cdMode === ChangeDetectorStatus.Destroyed) { this.throwDestroyedError('detectChanges'); } this.detectChangesInternal(throwOnChange); if (this.cdMode === ChangeDetectorStatus.CheckOnce) this.cdMode = ChangeDetectorStatus.Checked; this.numberOfChecks++; wtfLeave(s); } /** * Overwritten by implementations */ detectChangesInternal(throwOnChange: boolean): void {} markAsCheckOnce(): void { this.cdMode = ChangeDetectorStatus.CheckOnce; } markPathToRootAsCheckOnce(): void { let c: AppView = this; while (isPresent(c) && c.cdMode !== ChangeDetectorStatus.Detached) { if (c.cdMode === ChangeDetectorStatus.Checked) { c.cdMode = ChangeDetectorStatus.CheckOnce; } if (c.type === ViewType.COMPONENT) { c = c.parentView; } else { c = c.viewContainer ? c.viewContainer.parentView : null; } } } eventHandler(cb: (eventName: string, event?: E) => R): (eventName: string, event?: E) => R { return cb; } throwDestroyedError(details: string): void { throw new ViewDestroyedError(details); } } export class DebugAppView extends AppView { private _currentDebugContext: DebugContext = null; constructor( clazz: any, componentType: RenderComponentType, type: ViewType, viewUtils: ViewUtils, parentView: AppView, parentIndex: number, parentNode: any, cdMode: ChangeDetectorStatus, public staticNodeDebugInfos: StaticNodeDebugInfo[], declaredViewContainer: ViewContainer = null) { super( clazz, componentType, type, viewUtils, parentView, parentIndex, parentNode, cdMode, declaredViewContainer); } create(context: T) { this._resetDebug(); try { return super.create(context); } catch (e) { this._rethrowWithContext(e); throw e; } } createHostView( rootSelectorOrNode: string|any, injector: Injector, projectableNodes: any[][] = null): ComponentRef { this._resetDebug(); try { return super.createHostView(rootSelectorOrNode, injector, projectableNodes); } catch (e) { this._rethrowWithContext(e); throw e; } } injectorGet(token: any, nodeIndex: number, notFoundResult?: any): any { this._resetDebug(); try { return super.injectorGet(token, nodeIndex, notFoundResult); } catch (e) { this._rethrowWithContext(e); throw e; } } detach(): void { this._resetDebug(); try { super.detach(); } catch (e) { this._rethrowWithContext(e); throw e; } } destroy() { this._resetDebug(); try { super.destroy(); } catch (e) { this._rethrowWithContext(e); throw e; } } detectChanges(throwOnChange: boolean): void { this._resetDebug(); try { super.detectChanges(throwOnChange); } catch (e) { this._rethrowWithContext(e); throw e; } } private _resetDebug() { this._currentDebugContext = null; } debug(nodeIndex: number, rowNum: number, colNum: number): DebugContext { return this._currentDebugContext = new DebugContext(this, nodeIndex, rowNum, colNum); } private _rethrowWithContext(e: any) { if (!(e instanceof ViewWrappedError)) { if (!(e instanceof ExpressionChangedAfterItHasBeenCheckedError)) { this.cdMode = ChangeDetectorStatus.Errored; } if (isPresent(this._currentDebugContext)) { throw new ViewWrappedError(e, this._currentDebugContext); } } } eventHandler(cb: (eventName: string, event?: E) => R): (eventName: string, event?: E) => R { const superHandler = super.eventHandler(cb); return (eventName: string, event?: any) => { this._resetDebug(); try { return superHandler.call(this, eventName, event); } catch (e) { this._rethrowWithContext(e); throw e; } }; } }