BREAKING CHANGE: - Before, a `EmbeddedViewRef` used to have methods for setting variables. Now, a user has to pass in a context object that represents all variables when an `EmbeddedViewRef` should be created. - `ViewContainerRef.createEmbeddedViewRef` now takes a context object as 2nd argument. - `EmbeddedViewRef.setLocal` and `getLocal` have been removed. Use `EmbeddedViewRef.context` to access the context. - `DebugNode.locals` has been removed. Use the new methods `DebugElement.references` to get the references that are present on this element, or `DebugElement.context` to get the context of the `EmbeddedViewRef` or the component to which the element belongs. Closes #8321
		
			
				
	
	
		
			414 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			414 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import {
 | |
|   ListWrapper,
 | |
|   MapWrapper,
 | |
|   Map,
 | |
|   StringMapWrapper,
 | |
|   isListLikeIterable,
 | |
|   areIterablesEqual
 | |
| } from 'angular2/src/facade/collection';
 | |
| 
 | |
| import {Injector} from 'angular2/src/core/di';
 | |
| import {AppElement} from './element';
 | |
| import {
 | |
|   assertionsEnabled,
 | |
|   isPresent,
 | |
|   isBlank,
 | |
|   Type,
 | |
|   isArray,
 | |
|   isNumber,
 | |
|   CONST,
 | |
|   CONST_EXPR,
 | |
|   stringify,
 | |
|   isPrimitive,
 | |
|   isString
 | |
| } from 'angular2/src/facade/lang';
 | |
| 
 | |
| import {ObservableWrapper} from 'angular2/src/facade/async';
 | |
| import {Renderer, RootRenderer, RenderComponentType} from 'angular2/src/core/render/api';
 | |
| import {ViewRef_} from './view_ref';
 | |
| 
 | |
| import {ViewType} from './view_type';
 | |
| import {
 | |
|   ViewUtils,
 | |
|   flattenNestedViewRenderNodes,
 | |
|   ensureSlotCount,
 | |
|   arrayLooseIdentical,
 | |
|   mapLooseIdentical
 | |
| } from './view_utils';
 | |
| import {
 | |
|   ChangeDetectorRef,
 | |
|   ChangeDetectionStrategy,
 | |
|   ChangeDetectorState,
 | |
|   isDefaultChangeDetectionStrategy,
 | |
|   devModeEqual
 | |
| } from 'angular2/src/core/change_detection/change_detection';
 | |
| import {wtfCreateScope, wtfLeave, WtfScopeFn} from '../profile/profile';
 | |
| import {
 | |
|   ExpressionChangedAfterItHasBeenCheckedException,
 | |
|   ViewDestroyedException,
 | |
|   ViewWrappedException
 | |
| } from './exceptions';
 | |
| import {StaticNodeDebugInfo, DebugContext} from './debug_context';
 | |
| import {ElementInjector} from './element_injector';
 | |
| 
 | |
| var _scope_check: WtfScopeFn = wtfCreateScope(`AppView#check(ascii id)`);
 | |
| 
 | |
| /**
 | |
|  * Cost of making objects: http://jsperf.com/instantiate-size-of-object
 | |
|  *
 | |
|  */
 | |
| export abstract class AppView<T> {
 | |
|   ref: ViewRef_<T>;
 | |
|   rootNodesOrAppElements: any[];
 | |
|   allNodes: any[];
 | |
|   disposables: Function[];
 | |
|   subscriptions: any[];
 | |
|   contentChildren: AppView<any>[] = [];
 | |
|   viewChildren: AppView<any>[] = [];
 | |
|   renderParent: AppView<any>;
 | |
|   viewContainerElement: AppElement = null;
 | |
| 
 | |
|   // The names of the below fields must be kept in sync with codegen_name_util.ts or
 | |
|   // change detection will fail.
 | |
|   cdState: ChangeDetectorState = ChangeDetectorState.NeverChecked;
 | |
| 
 | |
|   projectableNodes: Array<any | any[]>;
 | |
| 
 | |
|   destroyed: boolean = false;
 | |
| 
 | |
|   renderer: Renderer;
 | |
| 
 | |
|   private _currentDebugContext: DebugContext = null;
 | |
| 
 | |
|   private _hasExternalHostElement: boolean;
 | |
| 
 | |
|   public context: T;
 | |
| 
 | |
|   constructor(public clazz: any, public componentType: RenderComponentType, public type: ViewType,
 | |
|               public viewUtils: ViewUtils, public parentInjector: Injector,
 | |
|               public declarationAppElement: AppElement, public cdMode: ChangeDetectionStrategy,
 | |
|               public staticNodeDebugInfos: StaticNodeDebugInfo[]) {
 | |
|     this.ref = new ViewRef_(this);
 | |
|     if (type === ViewType.COMPONENT || type === ViewType.HOST) {
 | |
|       this.renderer = viewUtils.renderComponent(componentType);
 | |
|     } else {
 | |
|       this.renderer = declarationAppElement.parentView.renderer;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   create(context: T, givenProjectableNodes: Array<any | any[]>,
 | |
|          rootSelectorOrNode: string | any): AppElement {
 | |
|     this.context = context;
 | |
|     var projectableNodes;
 | |
|     switch (this.type) {
 | |
|       case ViewType.COMPONENT:
 | |
|         projectableNodes = ensureSlotCount(givenProjectableNodes, this.componentType.slotCount);
 | |
|         break;
 | |
|       case ViewType.EMBEDDED:
 | |
|         projectableNodes = this.declarationAppElement.parentView.projectableNodes;
 | |
|         break;
 | |
|       case ViewType.HOST:
 | |
|         // Note: Don't ensure the slot count for the projectableNodes as we store
 | |
|         // them only for the contained component view (which will later check the slot count...)
 | |
|         projectableNodes = givenProjectableNodes;
 | |
|         break;
 | |
|     }
 | |
|     this._hasExternalHostElement = isPresent(rootSelectorOrNode);
 | |
|     this.projectableNodes = projectableNodes;
 | |
|     if (this.debugMode) {
 | |
|       this._resetDebug();
 | |
|       try {
 | |
|         return this.createInternal(rootSelectorOrNode);
 | |
|       } catch (e) {
 | |
|         this._rethrowWithContext(e, e.stack);
 | |
|         throw e;
 | |
|       }
 | |
|     } else {
 | |
|       return this.createInternal(rootSelectorOrNode);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Overwritten by implementations.
 | |
|    * Returns the AppElement for the host element for ViewType.HOST.
 | |
|    */
 | |
|   createInternal(rootSelectorOrNode: string | any): AppElement { return null; }
 | |
| 
 | |
|   init(rootNodesOrAppElements: any[], allNodes: any[], disposables: Function[],
 | |
|        subscriptions: any[]) {
 | |
|     this.rootNodesOrAppElements = rootNodesOrAppElements;
 | |
|     this.allNodes = allNodes;
 | |
|     this.disposables = disposables;
 | |
|     this.subscriptions = subscriptions;
 | |
|     if (this.type === ViewType.COMPONENT) {
 | |
|       // Note: the render nodes have been attached to their host element
 | |
|       // in the ViewFactory already.
 | |
|       this.declarationAppElement.parentView.viewChildren.push(this);
 | |
|       this.renderParent = this.declarationAppElement.parentView;
 | |
|       this.dirtyParentQueriesInternal();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   selectOrCreateHostElement(elementName: string, rootSelectorOrNode: string | any,
 | |
|                             debugCtx: DebugContext): any {
 | |
|     var hostElement;
 | |
|     if (isPresent(rootSelectorOrNode)) {
 | |
|       hostElement = this.renderer.selectRootElement(rootSelectorOrNode, debugCtx);
 | |
|     } else {
 | |
|       hostElement = this.renderer.createElement(null, elementName, debugCtx);
 | |
|     }
 | |
|     return hostElement;
 | |
|   }
 | |
| 
 | |
|   injectorGet(token: any, nodeIndex: number, notFoundResult: any): any {
 | |
|     if (this.debugMode) {
 | |
|       this._resetDebug();
 | |
|       try {
 | |
|         return this.injectorGetInternal(token, nodeIndex, notFoundResult);
 | |
|       } catch (e) {
 | |
|         this._rethrowWithContext(e, e.stack);
 | |
|         throw e;
 | |
|       }
 | |
|     } else {
 | |
|       return this.injectorGetInternal(token, nodeIndex, notFoundResult);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Overwritten by implementations
 | |
|    */
 | |
|   injectorGetInternal(token: any, nodeIndex: number, notFoundResult: any): any {
 | |
|     return notFoundResult;
 | |
|   }
 | |
| 
 | |
|   injector(nodeIndex: number): Injector {
 | |
|     if (isPresent(nodeIndex)) {
 | |
|       return new ElementInjector(this, nodeIndex);
 | |
|     } else {
 | |
|       return this.parentInjector;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   destroy() {
 | |
|     if (this._hasExternalHostElement) {
 | |
|       this.renderer.detachView(this.flatRootNodes);
 | |
|     } else if (isPresent(this.viewContainerElement)) {
 | |
|       this.viewContainerElement.detachView(this.viewContainerElement.nestedViews.indexOf(this));
 | |
|     }
 | |
|     this._destroyRecurse();
 | |
|   }
 | |
| 
 | |
|   private _destroyRecurse() {
 | |
|     if (this.destroyed) {
 | |
|       return;
 | |
|     }
 | |
|     var children = this.contentChildren;
 | |
|     for (var i = 0; i < children.length; i++) {
 | |
|       children[i]._destroyRecurse();
 | |
|     }
 | |
|     children = this.viewChildren;
 | |
|     for (var i = 0; i < children.length; i++) {
 | |
|       children[i]._destroyRecurse();
 | |
|     }
 | |
|     if (this.debugMode) {
 | |
|       this._resetDebug();
 | |
|       try {
 | |
|         this._destroyLocal();
 | |
|       } catch (e) {
 | |
|         this._rethrowWithContext(e, e.stack);
 | |
|         throw e;
 | |
|       }
 | |
|     } else {
 | |
|       this._destroyLocal();
 | |
|     }
 | |
| 
 | |
|     this.destroyed = true;
 | |
|   }
 | |
| 
 | |
|   private _destroyLocal() {
 | |
|     var hostElement =
 | |
|         this.type === ViewType.COMPONENT ? this.declarationAppElement.nativeElement : null;
 | |
|     for (var i = 0; i < this.disposables.length; i++) {
 | |
|       this.disposables[i]();
 | |
|     }
 | |
|     for (var i = 0; i < this.subscriptions.length; i++) {
 | |
|       ObservableWrapper.dispose(this.subscriptions[i]);
 | |
|     }
 | |
|     this.destroyInternal();
 | |
|     if (this._hasExternalHostElement) {
 | |
|       this.renderer.detachView(this.flatRootNodes);
 | |
|     } else if (isPresent(this.viewContainerElement)) {
 | |
|       this.viewContainerElement.detachView(this.viewContainerElement.nestedViews.indexOf(this));
 | |
|     } else {
 | |
|       this.dirtyParentQueriesInternal();
 | |
|     }
 | |
|     this.renderer.destroyView(hostElement, this.allNodes);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Overwritten by implementations
 | |
|    */
 | |
|   destroyInternal(): void {}
 | |
| 
 | |
|   get debugMode(): boolean { return isPresent(this.staticNodeDebugInfos); }
 | |
| 
 | |
|   get changeDetectorRef(): ChangeDetectorRef { return this.ref; }
 | |
| 
 | |
|   get parent(): AppView<any> {
 | |
|     return isPresent(this.declarationAppElement) ? this.declarationAppElement.parentView : null;
 | |
|   }
 | |
| 
 | |
|   get flatRootNodes(): any[] { return flattenNestedViewRenderNodes(this.rootNodesOrAppElements); }
 | |
| 
 | |
|   get lastRootNode(): any {
 | |
|     var lastNode = this.rootNodesOrAppElements.length > 0 ?
 | |
|                        this.rootNodesOrAppElements[this.rootNodesOrAppElements.length - 1] :
 | |
|                        null;
 | |
|     return _findLastRenderNode(lastNode);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Overwritten by implementations
 | |
|    */
 | |
|   dirtyParentQueriesInternal(): void {}
 | |
| 
 | |
|   addRenderContentChild(view: AppView<any>): void {
 | |
|     this.contentChildren.push(view);
 | |
|     view.renderParent = this;
 | |
|     view.dirtyParentQueriesInternal();
 | |
|   }
 | |
| 
 | |
|   removeContentChild(view: AppView<any>): void {
 | |
|     ListWrapper.remove(this.contentChildren, view);
 | |
|     view.dirtyParentQueriesInternal();
 | |
|     view.renderParent = null;
 | |
|   }
 | |
| 
 | |
|   detectChanges(throwOnChange: boolean): void {
 | |
|     var s = _scope_check(this.clazz);
 | |
|     if (this.cdMode === ChangeDetectionStrategy.Detached ||
 | |
|         this.cdMode === ChangeDetectionStrategy.Checked ||
 | |
|         this.cdState === ChangeDetectorState.Errored)
 | |
|       return;
 | |
|     if (this.destroyed) {
 | |
|       this.throwDestroyedError('detectChanges');
 | |
|     }
 | |
|     if (this.debugMode) {
 | |
|       this._resetDebug();
 | |
|       try {
 | |
|         this.detectChangesInternal(throwOnChange);
 | |
|       } catch (e) {
 | |
|         this._rethrowWithContext(e, e.stack);
 | |
|         throw e;
 | |
|       }
 | |
|     } else {
 | |
|       this.detectChangesInternal(throwOnChange);
 | |
|     }
 | |
|     if (this.cdMode === ChangeDetectionStrategy.CheckOnce)
 | |
|       this.cdMode = ChangeDetectionStrategy.Checked;
 | |
| 
 | |
|     this.cdState = ChangeDetectorState.CheckedBefore;
 | |
|     wtfLeave(s);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Overwritten by implementations
 | |
|    */
 | |
|   detectChangesInternal(throwOnChange: boolean): void {
 | |
|     this.detectContentChildrenChanges(throwOnChange);
 | |
|     this.detectViewChildrenChanges(throwOnChange);
 | |
|   }
 | |
| 
 | |
|   detectContentChildrenChanges(throwOnChange: boolean) {
 | |
|     for (var i = 0; i < this.contentChildren.length; ++i) {
 | |
|       this.contentChildren[i].detectChanges(throwOnChange);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   detectViewChildrenChanges(throwOnChange: boolean) {
 | |
|     for (var i = 0; i < this.viewChildren.length; ++i) {
 | |
|       this.viewChildren[i].detectChanges(throwOnChange);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   addToContentChildren(renderAppElement: AppElement): void {
 | |
|     renderAppElement.parentView.contentChildren.push(this);
 | |
|     this.viewContainerElement = renderAppElement;
 | |
|     this.dirtyParentQueriesInternal();
 | |
|   }
 | |
| 
 | |
|   removeFromContentChildren(renderAppElement: AppElement): void {
 | |
|     ListWrapper.remove(renderAppElement.parentView.contentChildren, this);
 | |
|     this.dirtyParentQueriesInternal();
 | |
|     this.viewContainerElement = null;
 | |
|   }
 | |
| 
 | |
|   markAsCheckOnce(): void { this.cdMode = ChangeDetectionStrategy.CheckOnce; }
 | |
| 
 | |
|   markPathToRootAsCheckOnce(): void {
 | |
|     var c: AppView<any> = this;
 | |
|     while (isPresent(c) && c.cdMode !== ChangeDetectionStrategy.Detached) {
 | |
|       if (c.cdMode === ChangeDetectionStrategy.Checked) {
 | |
|         c.cdMode = ChangeDetectionStrategy.CheckOnce;
 | |
|       }
 | |
|       c = c.renderParent;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   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, stack: any) {
 | |
|     if (!(e instanceof ViewWrappedException)) {
 | |
|       if (!(e instanceof ExpressionChangedAfterItHasBeenCheckedException)) {
 | |
|         this.cdState = ChangeDetectorState.Errored;
 | |
|       }
 | |
|       if (isPresent(this._currentDebugContext)) {
 | |
|         throw new ViewWrappedException(e, stack, this._currentDebugContext);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   eventHandler(cb: Function): Function {
 | |
|     if (this.debugMode) {
 | |
|       return (event) => {
 | |
|         this._resetDebug();
 | |
|         try {
 | |
|           return cb(event);
 | |
|         } catch (e) {
 | |
|           this._rethrowWithContext(e, e.stack);
 | |
|           throw e;
 | |
|         }
 | |
|       };
 | |
|     } else {
 | |
|       return cb;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   throwDestroyedError(details: string): void { throw new ViewDestroyedException(details); }
 | |
| }
 | |
| 
 | |
| function _findLastRenderNode(node: any): any {
 | |
|   var lastNode;
 | |
|   if (node instanceof AppElement) {
 | |
|     var appEl = <AppElement>node;
 | |
|     lastNode = appEl.nativeElement;
 | |
|     if (isPresent(appEl.nestedViews)) {
 | |
|       // Note: Views might have no root nodes at all!
 | |
|       for (var i = appEl.nestedViews.length - 1; i >= 0; i--) {
 | |
|         var nestedView = appEl.nestedViews[i];
 | |
|         if (nestedView.rootNodesOrAppElements.length > 0) {
 | |
|           lastNode = _findLastRenderNode(
 | |
|               nestedView.rootNodesOrAppElements[nestedView.rootNodesOrAppElements.length - 1]);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   } else {
 | |
|     lastNode = node;
 | |
|   }
 | |
|   return lastNode;
 | |
| }
 |