From 52b21275f4c2c26c46627f5648b41a33bb5c8283 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Thu, 26 Jan 2017 17:07:37 -0800 Subject: [PATCH] feat(core): view engine - add debug information (#14197) Creates debug information for the renderer, and also reports errors relative to the declaration place in the template. Part of #14013 PR Close #14197 --- modules/@angular/core/src/view/element.ts | 31 +- modules/@angular/core/src/view/errors.ts | 43 +++ modules/@angular/core/src/view/index.ts | 3 +- modules/@angular/core/src/view/provider.ts | 38 ++- modules/@angular/core/src/view/query.ts | 46 +-- modules/@angular/core/src/view/services.ts | 99 +++++- modules/@angular/core/src/view/text.ts | 13 +- modules/@angular/core/src/view/types.ts | 42 ++- modules/@angular/core/src/view/util.ts | 101 ++++++- modules/@angular/core/src/view/view.ts | 284 +++++++++++------- .../@angular/core/test/view/anchor_spec.ts | 18 +- .../core/test/view/component_view_spec.ts | 16 +- .../@angular/core/test/view/element_spec.ts | 127 ++++---- .../core/test/view/embedded_view_spec.ts | 16 +- modules/@angular/core/test/view/helper.ts | 10 +- .../@angular/core/test/view/provider_spec.ts | 164 +++++++--- .../core/test/view/pure_expression_spec.ts | 33 +- modules/@angular/core/test/view/query_spec.ts | 92 ++++-- .../@angular/core/test/view/services_spec.ts | 99 ++++++ modules/@angular/core/test/view/text_spec.ts | 52 ++-- .../@angular/core/test/view/view_def_spec.ts | 2 +- modules/benchmarks/src/tree/ng2_next/index.ts | 3 +- modules/benchmarks/src/tree/ng2_next/tree.ts | 108 ++++--- 23 files changed, 1007 insertions(+), 433 deletions(-) create mode 100644 modules/@angular/core/src/view/errors.ts create mode 100644 modules/@angular/core/test/view/services_spec.ts diff --git a/modules/@angular/core/src/view/element.ts b/modules/@angular/core/src/view/element.ts index d298ef7756..7ff562321c 100644 --- a/modules/@angular/core/src/view/element.ts +++ b/modules/@angular/core/src/view/element.ts @@ -6,10 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ +import {isDevMode} from '../application_ref'; import {SecurityContext} from '../security'; -import {BindingDef, BindingType, DisposableFn, ElementData, ElementOutputDef, NodeData, NodeDef, NodeFlags, NodeType, QueryValueType, ViewData, ViewDefinition, ViewFlags, asElementData} from './types'; -import {checkAndUpdateBinding, setBindingDebugInfo} from './util'; +import {BindingDef, BindingType, DebugContext, DisposableFn, ElementData, ElementOutputDef, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, QueryValueType, ViewData, ViewDefinition, ViewFlags, asElementData} from './types'; +import {checkAndUpdateBinding, entryAction, setBindingDebugInfo, setCurrentNode, sliceErrorStack} from './util'; export function anchorDef( flags: NodeFlags, matchedQueries: [string, QueryValueType][], childCount: number, @@ -18,6 +19,8 @@ export function anchorDef( if (matchedQueries) { matchedQueries.forEach(([queryId, valueType]) => { matchedQueryDefs[queryId] = valueType; }); } + // skip the call to sliceErrorStack itself + the call to this function. + const source = isDevMode() ? sliceErrorStack(2, 3) : ''; return { type: NodeType.Element, // will bet set by the view definition @@ -38,7 +41,7 @@ export function anchorDef( attrs: undefined, outputs: [], template, // will bet set by the view definition - providerIndices: undefined, + providerIndices: undefined, source }, provider: undefined, text: undefined, @@ -54,6 +57,8 @@ export function elementDef( ([BindingType.ElementClass, string] | [BindingType.ElementStyle, string, string] | [BindingType.ElementAttribute | BindingType.ElementProperty, string, SecurityContext])[], outputs?: (string | [string, string])[]): NodeDef { + // skip the call to sliceErrorStack itself + the call to this function. + const source = isDevMode() ? sliceErrorStack(2, 3) : ''; const matchedQueryDefs: {[queryId: string]: QueryValueType} = {}; if (matchedQueries) { matchedQueries.forEach(([queryId, valueType]) => { matchedQueryDefs[queryId] = valueType; }); @@ -112,7 +117,7 @@ export function elementDef( outputs: outputDefs, template: undefined, // will bet set by the view definition - providerIndices: undefined, + providerIndices: undefined, source }, provider: undefined, text: undefined, @@ -127,8 +132,10 @@ export function createElement(view: ViewData, renderHost: any, def: NodeDef): El const elDef = def.element; let el: any; if (view.renderer) { - el = elDef.name ? view.renderer.createElement(parentNode, elDef.name) : - view.renderer.createTemplateAnchor(parentNode); + const debugContext = + isDevMode() ? view.services.createDebugContext(view, def.index) : undefined; + el = elDef.name ? view.renderer.createElement(parentNode, elDef.name, debugContext) : + view.renderer.createTemplateAnchor(parentNode, debugContext); } else { el = elDef.name ? document.createElement(elDef.name) : document.createComment(''); if (parentNode) { @@ -183,18 +190,22 @@ export function createElement(view: ViewData, renderHost: any, def: NodeDef): El } function renderEventHandlerClosure(view: ViewData, index: number, eventName: string) { - return (event: any) => { return view.def.handleEvent(view, index, eventName, event); }; + return entryAction(EntryAction.HandleEvent, (event: any) => { + setCurrentNode(view, index); + return view.def.handleEvent(view, index, eventName, event); + }); } function directDomEventHandlerClosure(view: ViewData, index: number, eventName: string) { - return (event: any) => { + return entryAction(EntryAction.HandleEvent, (event: any) => { + setCurrentNode(view, index); const result = view.def.handleEvent(view, index, eventName, event); if (result === false) { event.preventDefault(); } return result; - }; + }); } export function checkAndUpdateElementInline( @@ -314,7 +325,7 @@ function setElementProperty( let renderValue = securityContext ? view.services.sanitize(securityContext, value) : value; if (view.renderer) { view.renderer.setElementProperty(renderNode, name, renderValue); - if (view.def.flags & ViewFlags.LogBindingUpdate) { + if (isDevMode() && (view.def.flags & ViewFlags.DirectDom) === 0) { setBindingDebugInfo(view.renderer, renderNode, name, renderValue); } } else { diff --git a/modules/@angular/core/src/view/errors.ts b/modules/@angular/core/src/view/errors.ts new file mode 100644 index 0000000000..55b93ad965 --- /dev/null +++ b/modules/@angular/core/src/view/errors.ts @@ -0,0 +1,43 @@ +/** + * @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 {BaseError, WrappedError} from '../facade/errors'; + +import {DebugContext} from './types'; + +export function expressionChangedAfterItHasBeenCheckedError( + context: DebugContext, oldValue: any, currValue: any, isFirstCheck: boolean): ViewError { + let msg = + `Expression has changed after it was checked. Previous value: '${oldValue}'. Current value: '${currValue}'.`; + if (isFirstCheck) { + msg += + ` It seems like the view has been created after its parent and its children have been dirty checked.` + + ` Has it been created in a change detection hook ?`; + } + return viewError(msg, context); +} + +export function viewWrappedError(originalError: any, context: DebugContext): WrappedError& + ViewError { + const err = viewError(originalError.message, context) as WrappedError & ViewError; + err.originalError = originalError; + return err; +} + +export interface ViewError { context: DebugContext; } + +export function viewError(msg: string, context: DebugContext): ViewError { + const err = new Error(msg) as any; + err.context = context; + err.stack = context.source; + return err; +} + +export function isViewError(err: any): boolean { + return err.context; +} diff --git a/modules/@angular/core/src/view/index.ts b/modules/@angular/core/src/view/index.ts index 360b535ad1..4d0efe6e6a 100644 --- a/modules/@angular/core/src/view/index.ts +++ b/modules/@angular/core/src/view/index.ts @@ -11,7 +11,8 @@ export {providerDef} from './provider'; export {pureArrayDef, pureObjectDef, purePipeDef} from './pure_expression'; export {queryDef} from './query'; export {textDef} from './text'; -export {checkAndUpdateView, checkNoChangesView, createEmbeddedView, createRootView, destroyView, viewDef} from './view'; +export {setCurrentNode} from './util'; +export {checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createEmbeddedView, createRootView, destroyView, viewDef} from './view'; export {attachEmbeddedView, detachEmbeddedView, rootRenderNodes} from './view_attach'; export * from './types'; diff --git a/modules/@angular/core/src/view/provider.ts b/modules/@angular/core/src/view/provider.ts index 35c80fda69..c7efd47b5c 100644 --- a/modules/@angular/core/src/view/provider.ts +++ b/modules/@angular/core/src/view/provider.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {isDevMode} from '../application_ref'; import {SimpleChange, SimpleChanges} from '../change_detection/change_detection'; import {Injector} from '../di'; import {stringify} from '../facade/lang'; @@ -13,10 +14,10 @@ import {ElementRef} from '../linker/element_ref'; import {TemplateRef} from '../linker/template_ref'; import {ViewContainerRef} from '../linker/view_container_ref'; import {Renderer} from '../render/api'; -import {queryDef} from './query'; -import {BindingDef, BindingType, DepDef, DepFlags, DisposableFn, NodeData, NodeDef, NodeFlags, NodeType, ProviderData, ProviderOutputDef, QueryBindingType, QueryDef, QueryValueType, Services, ViewData, ViewDefinition, ViewFlags, asElementData, asProviderData} from './types'; -import {checkAndUpdateBinding, checkAndUpdateBindingWithChange, setBindingDebugInfo} from './util'; +import {queryDef} from './query'; +import {BindingDef, BindingType, DepDef, DepFlags, DisposableFn, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ProviderData, ProviderOutputDef, QueryBindingType, QueryDef, QueryValueType, Services, ViewData, ViewDefinition, ViewFlags, asElementData, asProviderData} from './types'; +import {checkAndUpdateBinding, checkAndUpdateBindingWithChange, entryAction, setBindingDebugInfo, setCurrentNode} from './util'; const _tokenKeyCache = new Map(); @@ -82,7 +83,12 @@ export function providerDef( matchedQueries: matchedQueryDefs, childCount, bindings, disposableCount: outputDefs.length, element: undefined, - provider: {tokenKey: tokenKey(ctor), ctor, deps: depDefs, outputs: outputDefs, component}, + provider: { + tokenKey: tokenKey(ctor), + token: ctor, ctor, + deps: depDefs, + outputs: outputDefs, component + }, text: undefined, pureExpression: undefined, query: undefined @@ -106,13 +112,20 @@ export function createProvider( for (let i = 0; i < providerDef.outputs.length; i++) { const output = providerDef.outputs[i]; const subscription = provider[output.propName].subscribe( - view.def.handleEvent.bind(null, view, def.parent, output.eventName)); + eventHandlerClosure(view, def.parent, output.eventName)); view.disposables[def.disposableIndex + i] = subscription.unsubscribe.bind(subscription); } } return {instance: provider, componentView: componentView}; } +function eventHandlerClosure(view: ViewData, index: number, eventName: string) { + return entryAction(EntryAction.HandleEvent, (event: any) => { + setCurrentNode(view, index); + view.def.handleEvent(view, index, eventName, event); + }); +} + export function checkAndUpdateProviderInline( view: ViewData, def: NodeDef, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, v7: any, v8: any, v9: any) { @@ -239,6 +252,18 @@ export function resolveDep( return Injector.NULL.get(depDef.token, notFoundValue); } +export function createInjector(view: ViewData, elIndex: number): Injector { + return new Injector_(view, elIndex); +} + +class Injector_ implements Injector { + constructor(private view: ViewData, private elIndex: number) {} + get(token: any, notFoundValue?: any): any { + return resolveDep( + this.view, this.elIndex, {flags: DepFlags.None, token, tokenKey: tokenKey(token)}); + } +} + function checkAndUpdateProp( view: ViewData, provider: any, def: NodeDef, bindingIdx: number, value: any, changes: SimpleChanges): SimpleChanges { @@ -258,7 +283,7 @@ function checkAndUpdateProp( // so Closure Compiler will have renamed the property correctly already. provider[propName] = value; - if (view.def.flags & ViewFlags.LogBindingUpdate) { + if (isDevMode() && (view.def.flags & ViewFlags.DirectDom) === 0) { setBindingDebugInfo( view.renderer, asElementData(view, def.parent).renderElement, binding.nonMinifiedName, value); @@ -282,6 +307,7 @@ export function callLifecycleHooksChildrenFirst(view: ViewData, lifecycles: Node const nodeIndex = nodeDef.index; if (nodeDef.flags & lifecycles) { // a leaf + setCurrentNode(view, nodeIndex); callProviderLifecycles(asProviderData(view, nodeIndex).instance, nodeDef.flags & lifecycles); } else if ((nodeDef.childFlags & lifecycles) === 0) { // a parent with leafs diff --git a/modules/@angular/core/src/view/query.ts b/modules/@angular/core/src/view/query.ts index 55d319fa6d..4a5b6f8e12 100644 --- a/modules/@angular/core/src/view/query.ts +++ b/modules/@angular/core/src/view/query.ts @@ -7,7 +7,6 @@ */ import {ElementRef} from '../linker/element_ref'; -import {ExpressionChangedAfterItHasBeenCheckedError} from '../linker/errors'; import {QueryList} from '../linker/query_list'; import {TemplateRef} from '../linker/template_ref'; import {ViewContainerRef} from '../linker/view_container_ref'; @@ -110,24 +109,9 @@ function calcQueryValues( const len = view.def.nodes.length; for (let i = startIndex; i <= endIndex; i++) { const nodeDef = view.def.nodes[i]; - const queryValueType = nodeDef.matchedQueries[queryId]; - if (queryValueType != null) { + const value = getQueryValue(view, nodeDef, queryId); + if (value != null) { // a match - let value: any; - switch (queryValueType) { - case QueryValueType.ElementRef: - value = new ElementRef(asElementData(view, i).renderElement); - break; - case QueryValueType.TemplateRef: - value = view.services.createTemplateRef(view, nodeDef); - break; - case QueryValueType.ViewContainerRef: - value = view.services.createViewContainerRef(asElementData(view, i)); - break; - case QueryValueType.Provider: - value = asProviderData(view, i).instance; - break; - } values.push(value); } if (nodeDef.flags & NodeFlags.HasEmbeddedViews && @@ -158,3 +142,29 @@ function calcQueryValues( } return values; } + +export function getQueryValue(view: ViewData, nodeDef: NodeDef, queryId: string): any { + const queryValueType = nodeDef.matchedQueries[queryId]; + if (queryValueType != null) { + // a match + let value: any; + switch (queryValueType) { + case QueryValueType.RenderElement: + value = asElementData(view, nodeDef.index).renderElement; + break; + case QueryValueType.ElementRef: + value = new ElementRef(asElementData(view, nodeDef.index).renderElement); + break; + case QueryValueType.TemplateRef: + value = view.services.createTemplateRef(view, nodeDef); + break; + case QueryValueType.ViewContainerRef: + value = view.services.createViewContainerRef(asElementData(view, nodeDef.index)); + break; + case QueryValueType.Provider: + value = asProviderData(view, nodeDef.index).instance; + break; + } + return value; + } +} \ No newline at end of file diff --git a/modules/@angular/core/src/view/services.ts b/modules/@angular/core/src/view/services.ts index 7114c0a48a..796b8e3446 100644 --- a/modules/@angular/core/src/view/services.ts +++ b/modules/@angular/core/src/view/services.ts @@ -16,7 +16,10 @@ import {EmbeddedViewRef, ViewRef} from '../linker/view_ref'; import {RenderComponentType, Renderer, RootRenderer} from '../render/api'; import {Sanitizer, SecurityContext} from '../security'; -import {ElementData, NodeData, NodeDef, Services, ViewData, ViewDefinition, asElementData} from './types'; +import {createInjector} from './provider'; +import {getQueryValue} from './query'; +import {DebugContext, ElementData, NodeData, NodeDef, NodeType, Services, ViewData, ViewDefinition, asElementData} from './types'; +import {isComponentView, renderNode} from './util'; import {checkAndUpdateView, checkNoChangesView, createEmbeddedView, destroyView} from './view'; import {attachEmbeddedView, detachEmbeddedView, rootRenderNodes} from './view_attach'; @@ -30,15 +33,15 @@ export class DefaultServices implements Services { sanitize(context: SecurityContext, value: string): string { return this._sanitizer.sanitize(context, value); } - // Note: This needs to be here to prevent a cycle in source files. createViewContainerRef(data: ElementData): ViewContainerRef { return new ViewContainerRef_(data); } - - // Note: This needs to be here to prevent a cycle in source files. createTemplateRef(parentView: ViewData, def: NodeDef): TemplateRef { return new TemplateRef_(parentView, def); } + createDebugContext(view: ViewData, nodeIndex: number): DebugContext { + return new DebugContext_(view, nodeIndex); + } } class ViewContainerRef_ implements ViewContainerRef { @@ -132,3 +135,91 @@ class TemplateRef_ implements TemplateRef { return new ElementRef(asElementData(this._parentView, this._def.index).renderElement); } } + +class DebugContext_ implements DebugContext { + private nodeDef: NodeDef; + private elDef: NodeDef; + constructor(public view: ViewData, public nodeIndex: number) { + this.nodeDef = view.def.nodes[nodeIndex]; + this.elDef = findElementDef(view, nodeIndex); + } + get injector(): Injector { return createInjector(this.view, this.elDef.index); } + get component(): any { return this.view.component; } + get providerTokens(): any[] { + const tokens: any[] = []; + if (this.elDef) { + for (let i = this.elDef.index + 1; i <= this.elDef.index + this.elDef.childCount; i++) { + const childDef = this.view.def.nodes[i]; + if (childDef.type === NodeType.Provider) { + tokens.push(childDef.provider.token); + } else { + i += childDef.childCount; + } + } + } + return tokens; + } + get references(): {[key: string]: any} { + const references: {[key: string]: any} = {}; + if (this.elDef) { + collectReferences(this.view, this.elDef, references); + + for (let i = this.elDef.index + 1; i <= this.elDef.index + this.elDef.childCount; i++) { + const childDef = this.view.def.nodes[i]; + if (childDef.type === NodeType.Provider) { + collectReferences(this.view, childDef, references); + } else { + i += childDef.childCount; + } + } + } + return references; + } + get context(): any { return this.view.context; } + get source(): string { + if (this.nodeDef.type === NodeType.Text) { + return this.nodeDef.text.source; + } else { + return this.elDef.element.source; + } + } + get componentRenderElement() { + const elData = findHostElement(this.view); + return elData ? elData.renderElement : undefined; + } + get renderNode(): any { + let nodeDef = this.nodeDef.type === NodeType.Text ? this.nodeDef : this.elDef; + return renderNode(this.view, nodeDef); + } +} + +function findHostElement(view: ViewData): ElementData { + while (view && !isComponentView(view)) { + view = view.parent; + } + if (view.parent) { + const hostData = asElementData(view.parent, view.parentIndex); + return hostData; + } + return undefined; +} + +function findElementDef(view: ViewData, nodeIndex: number): NodeDef { + const viewDef = view.def; + let nodeDef = viewDef.nodes[nodeIndex]; + while (nodeDef) { + if (nodeDef.type === NodeType.Element) { + return nodeDef; + } + nodeDef = nodeDef.parent != null ? viewDef.nodes[nodeDef.parent] : undefined; + } + return undefined; +} + +function collectReferences(view: ViewData, nodeDef: NodeDef, references: {[key: string]: any}) { + for (let queryId in nodeDef.matchedQueries) { + if (queryId.startsWith('#')) { + references[queryId.slice(1)] = getQueryValue(view, nodeDef, queryId); + } + } +} \ No newline at end of file diff --git a/modules/@angular/core/src/view/text.ts b/modules/@angular/core/src/view/text.ts index 0291344765..fdde2c0719 100644 --- a/modules/@angular/core/src/view/text.ts +++ b/modules/@angular/core/src/view/text.ts @@ -6,12 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ +import {isDevMode} from '../application_ref'; import {looseIdentical} from '../facade/lang'; -import {BindingDef, BindingType, NodeData, NodeDef, NodeFlags, NodeType, Services, TextData, ViewData, asElementData, asTextData} from './types'; -import {checkAndUpdateBinding} from './util'; +import {BindingDef, BindingType, DebugContext, NodeData, NodeDef, NodeFlags, NodeType, Services, TextData, ViewData, ViewFlags, asElementData, asTextData} from './types'; +import {checkAndUpdateBinding, sliceErrorStack} from './util'; export function textDef(constants: string[]): NodeDef { + // skip the call to sliceErrorStack itself + the call to this function. + const source = isDevMode() ? sliceErrorStack(2, 3) : ''; const bindings: BindingDef[] = new Array(constants.length - 1); for (let i = 1; i < constants.length; i++) { bindings[i - 1] = { @@ -39,7 +42,7 @@ export function textDef(constants: string[]): NodeDef { disposableCount: 0, element: undefined, provider: undefined, - text: {prefix: constants[0]}, + text: {prefix: constants[0], source}, pureExpression: undefined, query: undefined, }; @@ -50,7 +53,9 @@ export function createText(view: ViewData, renderHost: any, def: NodeDef): TextD def.parent != null ? asElementData(view, def.parent).renderElement : renderHost; let renderNode: any; if (view.renderer) { - renderNode = view.renderer.createText(parentNode, def.text.prefix); + const debugContext = + isDevMode() ? view.services.createDebugContext(view, def.index) : undefined; + renderNode = view.renderer.createText(parentNode, def.text.prefix, debugContext); } else { renderNode = document.createTextNode(def.text.prefix); if (parentNode) { diff --git a/modules/@angular/core/src/view/types.ts b/modules/@angular/core/src/view/types.ts index 40903ee78b..29379b5135 100644 --- a/modules/@angular/core/src/view/types.ts +++ b/modules/@angular/core/src/view/types.ts @@ -10,7 +10,7 @@ import {PipeTransform} from '../change_detection/change_detection'; import {QueryList} from '../linker/query_list'; import {TemplateRef} from '../linker/template_ref'; import {ViewContainerRef} from '../linker/view_container_ref'; -import {RenderComponentType, Renderer, RootRenderer} from '../render/api'; +import {RenderComponentType, RenderDebugInfo, Renderer, RootRenderer} from '../render/api'; import {Sanitizer, SecurityContext} from '../security'; // ------------------------------------- @@ -44,14 +44,9 @@ export interface ViewDefinition { nodeMatchedQueries: {[queryId: string]: boolean}; } -export type ViewUpdateFn = (updater: NodeUpdater, view: ViewData) => void; +export type ViewDefinitionFactory = () => ViewDefinition; -export interface NodeUpdater { - checkInline( - view: ViewData, nodeIndex: number, v0?: any, v1?: any, v2?: any, v3?: any, v4?: any, v5?: any, - v6?: any, v7?: any, v8?: any, v9?: any): any; - checkDynamic(view: ViewData, nodeIndex: number, values: any[]): any; -} +export type ViewUpdateFn = (view: ViewData) => void; export type ViewHandleEventFn = (view: ViewData, nodeIndex: number, eventName: string, event: any) => boolean; @@ -61,7 +56,6 @@ export type ViewHandleEventFn = */ export enum ViewFlags { None = 0, - LogBindingUpdate = 1 << 0, DirectDom = 1 << 1 } @@ -149,6 +143,7 @@ export enum BindingType { export enum QueryValueType { ElementRef, + RenderElement, TemplateRef, ViewContainerRef, Provider @@ -166,6 +161,7 @@ export interface ElementDef { * to indices in parent ElementDefs. */ providerIndices: {[tokenKey: string]: number}; + source: string; } export interface ElementOutputDef { @@ -174,12 +170,13 @@ export interface ElementOutputDef { } export interface ProviderDef { + token: any; tokenKey: string; ctor: any; deps: DepDef[]; outputs: ProviderOutputDef[]; // closure to allow recursive components - component: () => ViewDefinition; + component: ViewDefinitionFactory; } export interface DepDef { @@ -201,7 +198,10 @@ export interface ProviderOutputDef { eventName: string; } -export interface TextDef { prefix: string; } +export interface TextDef { + prefix: string; + source: string; +} export interface PureExpressionDef { type: PureExpressionType; @@ -361,4 +361,24 @@ export interface Services { createViewContainerRef(data: ElementData): ViewContainerRef; // Note: This needs to be here to prevent a cycle in source files. createTemplateRef(parentView: ViewData, def: NodeDef): TemplateRef; + // Note: This needs to be here to prevent a cycle in source files. + createDebugContext(view: ViewData, nodeIndex: number): DebugContext; +} + +// ------------------------------------- +// Other +// ------------------------------------- +export enum EntryAction { + CheckAndUpdate, + CheckNoChanges, + Create, + Destroy, + HandleEvent +} + +export interface DebugContext extends RenderDebugInfo { + view: ViewData; + nodeIndex: number; + componentRenderElement: any; + renderNode: any; } diff --git a/modules/@angular/core/src/view/util.ts b/modules/@angular/core/src/view/util.ts index 17e8ee536f..8f641be90e 100644 --- a/modules/@angular/core/src/view/util.ts +++ b/modules/@angular/core/src/view/util.ts @@ -6,13 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ +import {isDevMode} from '../application_ref'; import {devModeEqual} from '../change_detection/change_detection'; import {SimpleChange} from '../change_detection/change_detection_util'; import {looseIdentical} from '../facade/lang'; -import {ExpressionChangedAfterItHasBeenCheckedError} from '../linker/errors'; import {Renderer} from '../render/api'; -import {ElementData, NodeData, NodeDef, NodeFlags, NodeType, ViewData, ViewDefinition, asElementData, asTextData} from './types'; +import {expressionChangedAfterItHasBeenCheckedError, isViewError, viewWrappedError} from './errors'; +import {ElementData, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ViewData, ViewDefinition, ViewDefinitionFactory, asElementData, asTextData} from './types'; export function setBindingDebugInfo( renderer: Renderer, renderNode: any, propName: string, value: any) { @@ -36,7 +37,8 @@ export function checkBindingNoChanges( view: ViewData, def: NodeDef, bindingIdx: number, value: any) { const oldValue = view.oldValues[def.bindingIndex + bindingIdx]; if (view.firstChange || !devModeEqual(oldValue, value)) { - throw new ExpressionChangedAfterItHasBeenCheckedError(oldValue, value, view.firstChange); + throw expressionChangedAfterItHasBeenCheckedError( + view.services.createDebugContext(view, def.index), oldValue, value, view.firstChange); } } @@ -76,4 +78,95 @@ export function renderNode(view: ViewData, def: NodeDef): any { case NodeType.Text: return asTextData(view, def.index).renderText; } -} \ No newline at end of file +} + +export function isComponentView(view: ViewData): boolean { + return view.component === view.context && !!view.parent; +} + +const VIEW_DEFINITION_CACHE = new WeakMap(); + +export function resolveViewDefinition(factory: ViewDefinitionFactory): ViewDefinition { + let value: ViewDefinition = VIEW_DEFINITION_CACHE.get(factory); + if (!value) { + value = factory(); + VIEW_DEFINITION_CACHE.set(factory, value); + } + return value; +} + +export function sliceErrorStack(start: number, end: number): string { + let err: any; + try { + throw new Error(); + } catch (e) { + err = e; + } + const stack = err.stack || ''; + const lines = stack.split('\n'); + if (lines[0].startsWith('Error')) { + // Chrome always adds the message to the stack as well... + start++; + end++; + } + return lines.slice(start, end).join('\n'); +} + +let _currentAction: EntryAction; +let _currentView: ViewData; +let _currentNodeIndex: number; + +export function currentView() { + return _currentView; +} + +export function currentNodeIndex() { + return _currentNodeIndex; +} + +export function currentAction() { + return _currentAction; +} + +/** + * Set the node that is currently worked on. + * It needs to be called whenever we call user code, + * or code of the framework that might throw as a valid use case. + */ +export function setCurrentNode(view: ViewData, nodeIndex: number) { + _currentView = view; + _currentNodeIndex = nodeIndex; +} + +/** + * Adds a try/catch handler around the given function to wrap all + * errors that occur into new errors that contain the current debug info + * set via setCurrentNode. + */ +export function entryAction(action: EntryAction, fn: (arg: A) => R): (arg: A) => R { + return function(arg: any) { + const oldAction = _currentAction; + const oldView = _currentView; + const oldNodeIndex = _currentNodeIndex; + _currentAction = action; + // Note: We can't call `isDevMode()` outside of this closure as + // it might not have been initialized. + const result = isDevMode() ? callWithTryCatch(fn, arg) : fn(arg); + _currentAction = oldAction; + _currentView = oldView; + _currentNodeIndex = oldNodeIndex; + return result; + }; +} + +function callWithTryCatch(fn: (a: any) => any, arg: any): any { + try { + return fn(arg); + } catch (e) { + if (isViewError(e) || !_currentView) { + throw e; + } + const debugContext = _currentView.services.createDebugContext(_currentView, _currentNodeIndex); + throw viewWrappedError(e, debugContext); + } +} diff --git a/modules/@angular/core/src/view/view.ts b/modules/@angular/core/src/view/view.ts index f2ef5bb204..b141de04d5 100644 --- a/modules/@angular/core/src/view/view.ts +++ b/modules/@angular/core/src/view/view.ts @@ -6,16 +6,17 @@ * found in the LICENSE file at https://angular.io/license */ -import {ExpressionChangedAfterItHasBeenCheckedError} from '../linker/errors'; +import {isDevMode} from '../application_ref'; import {RenderComponentType, Renderer} from '../render/api'; import {checkAndUpdateElementDynamic, checkAndUpdateElementInline, createElement} from './element'; +import {expressionChangedAfterItHasBeenCheckedError} from './errors'; import {callLifecycleHooksChildrenFirst, checkAndUpdateProviderDynamic, checkAndUpdateProviderInline, createProvider} from './provider'; import {checkAndUpdatePureExpressionDynamic, checkAndUpdatePureExpressionInline, createPureExpression} from './pure_expression'; import {checkAndUpdateQuery, createQuery, queryDef} from './query'; import {checkAndUpdateTextDynamic, checkAndUpdateTextInline, createText} from './text'; -import {ElementDef, NodeData, NodeDef, NodeFlags, NodeType, NodeUpdater, ProviderData, ProviderDef, Services, TextDef, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, asElementData, asProviderData, asPureExpressionData, asQueryList} from './types'; -import {checkBindingNoChanges} from './util'; +import {ElementDef, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ProviderData, ProviderDef, Services, TextDef, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewUpdateFn, asElementData, asProviderData, asPureExpressionData, asQueryList} from './types'; +import {checkBindingNoChanges, currentAction, currentNodeIndex, currentView, entryAction, isComponentView, resolveViewDefinition, setCurrentNode} from './util'; const NOOP = (): any => undefined; @@ -31,7 +32,7 @@ export function viewDef( const reverseChildNodes: NodeDef[] = new Array(nodesWithoutIndices.length); let viewBindingCount = 0; let viewDisposableCount = 0; - let viewFlags = 0; + let viewNodeFlags = 0; let viewMatchedQueries: {[queryId: string]: boolean} = {}; let currentParent: NodeDef = null; let lastRootNode: NodeDef = null; @@ -56,14 +57,15 @@ export function viewDef( }); if (node.element) { node.element = cloneAndModifyElement(node.element, { - providerIndices: Object.create(currentParent ? currentParent.element.providerIndices : null) + providerIndices: + Object.create(currentParent ? currentParent.element.providerIndices : null), }); } nodes[i] = node; reverseChildNodes[reverseChildIndex] = node; validateNode(currentParent, node); - viewFlags |= node.flags; + viewNodeFlags |= node.flags; copyInto(node.matchedQueries, viewMatchedQueries); viewBindingCount += node.bindings.length; viewDisposableCount += node.disposableCount; @@ -99,7 +101,7 @@ export function viewDef( } return { - nodeFlags: viewFlags, + nodeFlags: viewNodeFlags, nodeMatchedQueries: viewMatchedQueries, flags, nodes: nodes, reverseChildNodes, update: update || NOOP, @@ -222,13 +224,20 @@ export function createEmbeddedView(parent: ViewData, anchorDef: NodeDef, context // to get the parent of the anchor and use it as parentIndex. const view = createView( parent.services, parent, anchorDef.index, anchorDef.parent, anchorDef.element.template); - initView(view, null, parent.component, context); + initView(view, parent.component, context); + createViewNodes(view); return view; } -export function createRootView(services: Services, def: ViewDefinition, context?: any): ViewData { - const view = createView(services, null, null, null, def); - initView(view, null, context, context); +/** + * We take in a ViewDefinitionFactory, so that we can initialize the debug/prod mode first, + * and then know whether to capture error stacks in ElementDefs. + */ +export function createRootView( + services: Services, defFactory: ViewDefinitionFactory, context?: any): ViewData { + const view = createView(services, null, null, null, resolveViewDefinition(defFactory)); + initView(view, context, context); + createViewNodes(view); return view; } @@ -256,14 +265,31 @@ function createView( return view; } -function initView(view: ViewData, renderHost: any, component: any, context: any) { +function initView(view: ViewData, component: any, context: any) { view.component = component; view.context = context; +} + +const createViewNodes: (view: ViewData) => void = + entryAction(EntryAction.CheckNoChanges, _createViewNodes); + +function _createViewNodes(view: ViewData) { + let renderHost: any; + if (isComponentView(view)) { + renderHost = asElementData(view.parent, view.parentIndex).renderElement; + if (view.renderer) { + renderHost = view.renderer.createViewRoot(renderHost); + } + } + const def = view.def; const nodes = view.nodes; for (let i = 0; i < def.nodes.length; i++) { const nodeDef = def.nodes[i]; let nodeData: any; + // As the current node is being created, we have to use + // the parent node as the current node for error messages, ... + setCurrentNode(view, nodeDef.parent); switch (nodeDef.type) { case NodeType.Element: nodeData = createElement(view, renderHost, nodeDef); @@ -276,9 +302,13 @@ function initView(view: ViewData, renderHost: any, component: any, context: any) if (nodeDef.provider.component) { const hostElIndex = nodeDef.parent; componentView = createView( - view.services, view, hostElIndex, hostElIndex, nodeDef.provider.component()); + view.services, view, hostElIndex, hostElIndex, + resolveViewDefinition(nodeDef.provider.component)); + } + const providerData = nodeData = createProvider(view, nodeDef, componentView); + if (componentView) { + initView(componentView, providerData.instance, providerData.instance); } - nodeData = createProvider(view, nodeDef, componentView); break; case NodeType.PureExpression: nodeData = createPureExpression(view, nodeDef); @@ -289,63 +319,25 @@ function initView(view: ViewData, renderHost: any, component: any, context: any) } nodes[i] = nodeData; } - execComponentViewsAction(view, ViewAction.InitComponent); + execComponentViewsAction(view, ViewAction.CreateViewNodes); } -export function checkNoChangesView(view: ViewData) { - view.def.update(CheckNoChanges, view); +export const checkNoChangesView: (view: ViewData) => void = + entryAction(EntryAction.CheckNoChanges, _checkNoChangesView); + +function _checkNoChangesView(view: ViewData) { + view.def.update(view); execEmbeddedViewsAction(view, ViewAction.CheckNoChanges); execQueriesAction(view, NodeFlags.HasContentQuery, QueryAction.CheckNoChanges); execComponentViewsAction(view, ViewAction.CheckNoChanges); execQueriesAction(view, NodeFlags.HasViewQuery, QueryAction.CheckNoChanges); } -const CheckNoChanges: NodeUpdater = { - checkInline: (view: ViewData, index: number, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, - v6: any, v7: any, v8: any, v9: any): void => { - const nodeDef = view.def.nodes[index]; - // Note: fallthrough is intended! - switch (nodeDef.bindings.length) { - case 10: - checkBindingNoChanges(view, nodeDef, 9, v9); - case 9: - checkBindingNoChanges(view, nodeDef, 8, v8); - case 8: - checkBindingNoChanges(view, nodeDef, 7, v7); - case 7: - checkBindingNoChanges(view, nodeDef, 6, v6); - case 6: - checkBindingNoChanges(view, nodeDef, 5, v5); - case 5: - checkBindingNoChanges(view, nodeDef, 4, v4); - case 4: - checkBindingNoChanges(view, nodeDef, 3, v3); - case 3: - checkBindingNoChanges(view, nodeDef, 2, v2); - case 2: - checkBindingNoChanges(view, nodeDef, 1, v1); - case 1: - checkBindingNoChanges(view, nodeDef, 0, v0); - } - if (nodeDef.type === NodeType.PureExpression) { - return asPureExpressionData(view, index).value; - } - return undefined; - }, - checkDynamic: (view: ViewData, index: number, values: any[]): void => { - const nodeDef = view.def.nodes[index]; - for (let i = 0; i < values.length; i++) { - checkBindingNoChanges(view, nodeDef, i, values[i]); - } - if (nodeDef.type === NodeType.PureExpression) { - return asPureExpressionData(view, index).value; - } - return undefined; - } -}; +export const checkAndUpdateView: (view: ViewData) => void = + entryAction(EntryAction.CheckAndUpdate, _checkAndUpdateView); -export function checkAndUpdateView(view: ViewData) { - view.def.update(CheckAndUpdate, view); +function _checkAndUpdateView(view: ViewData) { + view.def.update(view); execEmbeddedViewsAction(view, ViewAction.CheckAndUpdate); execQueriesAction(view, NodeFlags.HasContentQuery, QueryAction.CheckAndUpdate); @@ -359,52 +351,121 @@ export function checkAndUpdateView(view: ViewData) { view.firstChange = false; } -const CheckAndUpdate: NodeUpdater = { - checkInline: (view: ViewData, index: number, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, - v6: any, v7: any, v8: any, v9: any): void => { - const nodeDef = view.def.nodes[index]; - switch (nodeDef.type) { - case NodeType.Element: - checkAndUpdateElementInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); - return undefined; - case NodeType.Text: - checkAndUpdateTextInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); - return undefined; - case NodeType.Provider: - checkAndUpdateProviderInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); - return undefined; - case NodeType.PureExpression: - checkAndUpdatePureExpressionInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); - return asPureExpressionData(view, index).value; - } - }, - checkDynamic: (view: ViewData, index: number, values: any[]): void => { - const nodeDef = view.def.nodes[index]; - switch (nodeDef.type) { - case NodeType.Element: - checkAndUpdateElementDynamic(view, nodeDef, values); - return undefined; - case NodeType.Text: - checkAndUpdateTextDynamic(view, nodeDef, values); - return undefined; - case NodeType.Provider: - checkAndUpdateProviderDynamic(view, nodeDef, values); - return undefined; - case NodeType.PureExpression: - checkAndUpdatePureExpressionDynamic(view, nodeDef, values); - return asPureExpressionData(view, index).value; - } +export function checkNodeInline( + v0?: any, v1?: any, v2?: any, v3?: any, v4?: any, v5?: any, v6?: any, v7?: any, v8?: any, + v9?: any): any { + const action = currentAction(); + const view = currentView(); + const nodeIndex = currentNodeIndex(); + const nodeDef = view.def.nodes[nodeIndex]; + switch (action) { + case EntryAction.CheckNoChanges: + checkNodeNoChangesInline(view, nodeIndex, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); + break; + case EntryAction.CheckAndUpdate: + switch (nodeDef.type) { + case NodeType.Element: + checkAndUpdateElementInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); + break; + case NodeType.Text: + checkAndUpdateTextInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); + break; + case NodeType.Provider: + checkAndUpdateProviderInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); + break; + case NodeType.PureExpression: + checkAndUpdatePureExpressionInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); + break; + } + break; + default: + throw new Error(`Illegal State: In action ${EntryAction[action]}`); } -}; + return nodeDef.type === NodeType.PureExpression ? asPureExpressionData(view, nodeIndex).value : + undefined; +} + +export function checkNodeDynamic(values: any[]): any { + const action = currentAction(); + const view = currentView(); + const nodeIndex = currentNodeIndex(); + const nodeDef = view.def.nodes[nodeIndex]; + switch (action) { + case EntryAction.CheckNoChanges: + checkNodeNoChangesDynamic(view, nodeIndex, values); + break; + case EntryAction.CheckAndUpdate: + switch (nodeDef.type) { + case NodeType.Element: + checkAndUpdateElementDynamic(view, nodeDef, values); + break; + case NodeType.Text: + checkAndUpdateTextDynamic(view, nodeDef, values); + break; + case NodeType.Provider: + checkAndUpdateProviderDynamic(view, nodeDef, values); + break; + case NodeType.PureExpression: + checkAndUpdatePureExpressionDynamic(view, nodeDef, values); + break; + } + break; + default: + throw new Error(`Illegal State: In action ${EntryAction[action]}`); + } + return nodeDef.type === NodeType.PureExpression ? asPureExpressionData(view, nodeIndex).value : + undefined; +} + +function checkNodeNoChangesInline( + view: ViewData, nodeIndex: number, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, + v6: any, v7: any, v8: any, v9: any): void { + const nodeDef = view.def.nodes[nodeIndex]; + // Note: fallthrough is intended! + switch (nodeDef.bindings.length) { + case 10: + checkBindingNoChanges(view, nodeDef, 9, v9); + case 9: + checkBindingNoChanges(view, nodeDef, 8, v8); + case 8: + checkBindingNoChanges(view, nodeDef, 7, v7); + case 7: + checkBindingNoChanges(view, nodeDef, 6, v6); + case 6: + checkBindingNoChanges(view, nodeDef, 5, v5); + case 5: + checkBindingNoChanges(view, nodeDef, 4, v4); + case 4: + checkBindingNoChanges(view, nodeDef, 3, v3); + case 3: + checkBindingNoChanges(view, nodeDef, 2, v2); + case 2: + checkBindingNoChanges(view, nodeDef, 1, v1); + case 1: + checkBindingNoChanges(view, nodeDef, 0, v0); + } + return undefined; +} + +function checkNodeNoChangesDynamic(view: ViewData, nodeIndex: number, values: any[]): void { + const nodeDef = view.def.nodes[nodeIndex]; + for (let i = 0; i < values.length; i++) { + checkBindingNoChanges(view, nodeDef, i, values[i]); + } +} function checkNoChangesQuery(view: ViewData, nodeDef: NodeDef) { const queryList = asQueryList(view, nodeDef.index); if (queryList.dirty) { - throw new ExpressionChangedAfterItHasBeenCheckedError(false, true, view.firstChange); + throw expressionChangedAfterItHasBeenCheckedError( + view.services.createDebugContext(view, nodeDef.index), + `Query ${nodeDef.query.id} not dirty`, `Query ${nodeDef.query.id} dirty`, view.firstChange); } } -export function destroyView(view: ViewData) { +export const destroyView: (view: ViewData) => void = entryAction(EntryAction.Destroy, _destroyView); + +function _destroyView(view: ViewData) { callLifecycleHooksChildrenFirst(view, NodeFlags.OnDestroy); if (view.disposables) { for (let i = 0; i < view.disposables.length; i++) { @@ -416,7 +477,7 @@ export function destroyView(view: ViewData) { } enum ViewAction { - InitComponent, + CreateViewNodes, CheckNoChanges, CheckAndUpdate, Destroy @@ -432,16 +493,7 @@ function execComponentViewsAction(view: ViewData, action: ViewAction) { if (nodeDef.flags & NodeFlags.HasComponent) { // a leaf const providerData = asProviderData(view, i); - if (action === ViewAction.InitComponent) { - let renderHost = asElementData(view, nodeDef.parent).renderElement; - if (view.renderer) { - renderHost = view.renderer.createViewRoot(renderHost); - } - initView( - providerData.componentView, renderHost, providerData.instance, providerData.instance); - } else { - callViewAction(providerData.componentView, action); - } + callViewAction(providerData.componentView, action); } else if ((nodeDef.childFlags & NodeFlags.HasComponent) === 0) { // a parent with leafs // no child is a component, @@ -478,13 +530,16 @@ function execEmbeddedViewsAction(view: ViewData, action: ViewAction) { function callViewAction(view: ViewData, action: ViewAction) { switch (action) { case ViewAction.CheckNoChanges: - checkNoChangesView(view); + _checkNoChangesView(view); break; case ViewAction.CheckAndUpdate: - checkAndUpdateView(view); + _checkAndUpdateView(view); break; case ViewAction.Destroy: - destroyView(view); + _destroyView(view); + break; + case ViewAction.CreateViewNodes: + _createViewNodes(view); break; } } @@ -502,6 +557,7 @@ function execQueriesAction(view: ViewData, queryFlags: NodeFlags, action: QueryA for (let i = 0; i < nodeCount; i++) { const nodeDef = view.def.nodes[i]; if (nodeDef.flags & queryFlags) { + setCurrentNode(view, nodeDef.index); switch (action) { case QueryAction.CheckAndUpdate: checkAndUpdateQuery(view, nodeDef); diff --git a/modules/@angular/core/test/view/anchor_spec.ts b/modules/@angular/core/test/view/anchor_spec.ts index b8bada556d..7480a9b864 100644 --- a/modules/@angular/core/test/view/anchor_spec.ts +++ b/modules/@angular/core/test/view/anchor_spec.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core'; -import {DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; +import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, getDebugNode} from '@angular/core'; +import {DebugContext, DefaultServices, NodeDef, NodeFlags, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createRootView, elementDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index'; import {inject} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; @@ -39,8 +39,9 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { return viewDef(config.viewFlags, nodes, update, handleEvent, renderComponentType); } - function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} { - const view = createRootView(services, viewDef); + function createAndGetRootNodes( + viewDef: ViewDefinition, ctx?: any): {rootNodes: any[], view: ViewData} { + const view = createRootView(services, () => viewDef, ctx); const rootNodes = rootRenderNodes(view); return {rootNodes, view}; } @@ -66,6 +67,15 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { ])).rootNodes; expect(getDOM().childNodes(rootNodes[0]).length).toBe(1); }); + + if (!config.directDom) { + it('should add debug information to the renderer', () => { + const someContext = new Object(); + const {view, rootNodes} = + createAndGetRootNodes(compViewDef([anchorDef(NodeFlags.None, null, 0)]), someContext); + expect(getDebugNode(rootNodes[0]).nativeNode).toBe(asElementData(view, 0).renderElement); + }); + } }); }); } diff --git a/modules/@angular/core/test/view/component_view_spec.ts b/modules/@angular/core/test/view/component_view_spec.ts index 4c323b4c32..8f0f8770af 100644 --- a/modules/@angular/core/test/view/component_view_spec.ts +++ b/modules/@angular/core/test/view/component_view_spec.ts @@ -7,7 +7,7 @@ */ import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core'; -import {BindingType, DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asProviderData, checkAndUpdateView, checkNoChangesView, createRootView, destroyView, elementDef, providerDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; +import {BindingType, DefaultServices, NodeDef, NodeFlags, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asProviderData, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createRootView, destroyView, elementDef, providerDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index'; import {inject} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; @@ -40,7 +40,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { } function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} { - const view = createRootView(services, viewDef); + const view = createRootView(services, () => viewDef); const rootNodes = rootRenderNodes(view); return {rootNodes, view}; } @@ -75,8 +75,10 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { a: any; } - const update = jasmine.createSpy('updater').and.callFake( - (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, value)); + const update = jasmine.createSpy('updater').and.callFake((view: ViewData) => { + setCurrentNode(view, 0); + checkNodeInline(value); + }); const {view, rootNodes} = createAndGetRootNodes( compViewDef([ @@ -91,14 +93,12 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { checkAndUpdateView(view); - expect(update).toHaveBeenCalled(); - expect(update.calls.mostRecent().args[1]).toBe(compView); + expect(update).toHaveBeenCalledWith(compView); update.calls.reset(); checkNoChangesView(view); - expect(update).toHaveBeenCalled(); - expect(update.calls.mostRecent().args[1]).toBe(compView); + expect(update).toHaveBeenCalledWith(compView); value = 'v2'; expect(() => checkNoChangesView(view)) diff --git a/modules/@angular/core/test/view/element_spec.ts b/modules/@angular/core/test/view/element_spec.ts index 81f52fe049..ff9cbe0182 100644 --- a/modules/@angular/core/test/view/element_spec.ts +++ b/modules/@angular/core/test/view/element_spec.ts @@ -6,12 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core'; -import {BindingType, DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, destroyView, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; +import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, getDebugNode} from '@angular/core'; +import {BindingType, DebugContext, DefaultServices, NodeDef, NodeFlags, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createRootView, destroyView, elementDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index'; import {inject} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; -import {isBrowser, setupAndCheckRenderer} from './helper'; +import {INLINE_DYNAMIC_VALUES, InlineDynamic, checkNodeInlineOrDynamic, isBrowser, setupAndCheckRenderer} from './helper'; export function main() { if (isBrowser()) { @@ -39,8 +39,9 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { return viewDef(config.viewFlags, nodes, update, handleEvent, renderComponentType); } - function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} { - const view = createRootView(services, viewDef); + function createAndGetRootNodes( + viewDef: ViewDefinition, context?: any): {rootNodes: any[], view: ViewData} { + const view = createRootView(services, () => viewDef, context); const rootNodes = rootRenderNodes(view); return {rootNodes, view}; } @@ -79,38 +80,20 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { expect(rootNodes.length).toBe(1); expect(getDOM().getAttribute(rootNodes[0], 'title')).toBe('a'); }); - }); - it('should checkNoChanges', () => { - let attrValue = 'v1'; - const {view, rootNodes} = createAndGetRootNodes(compViewDef( - [ - elementDef( - NodeFlags.None, null, 0, 'div', null, - [[BindingType.ElementAttribute, 'a1', SecurityContext.NONE]]), - ], - (updater, view) => updater.checkInline(view, 0, attrValue))); - - checkAndUpdateView(view); - checkNoChangesView(view); - - attrValue = 'v2'; - expect(() => checkNoChangesView(view)) - .toThrowError( - `Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`); + if (!config.directDom) { + it('should add debug information to the renderer', () => { + const someContext = new Object(); + const {view, rootNodes} = createAndGetRootNodes( + compViewDef([elementDef(NodeFlags.None, null, 0, 'div')]), someContext); + expect(getDebugNode(rootNodes[0]).nativeNode).toBe(asElementData(view, 0).renderElement); + }); + } }); describe('change properties', () => { - [{ - name: 'inline', - update: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 'v1', 'v2') - }, - { - name: 'dynamic', - update: (updater: NodeUpdater, view: ViewData) => - updater.checkDynamic(view, 0, ['v1', 'v2']) - }].forEach((config) => { - it(`should update ${config.name}`, () => { + INLINE_DYNAMIC_VALUES.forEach((inlineDynamic) => { + it(`should update ${InlineDynamic[inlineDynamic]}`, () => { const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ @@ -121,28 +104,27 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { [BindingType.ElementProperty, 'value', SecurityContext.NONE] ]), ], - config.update)); + (view) => { + setCurrentNode(view, 0); + checkNodeInlineOrDynamic(inlineDynamic, ['v1', 'v2']); + })); checkAndUpdateView(view); const el = rootNodes[0]; expect(getDOM().getProperty(el, 'title')).toBe('v1'); expect(getDOM().getProperty(el, 'value')).toBe('v2'); + + if (!config.directDom) { + expect(getDOM().getAttribute(el, 'ng-reflect-title')).toBe('v1'); + } }); }); }); describe('change attributes', () => { - [{ - name: 'inline', - update: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 'v1', 'v2') - }, - { - name: 'dynamic', - update: (updater: NodeUpdater, view: ViewData) => - updater.checkDynamic(view, 0, ['v1', 'v2']) - }].forEach((config) => { - it(`should update ${config.name}`, () => { + INLINE_DYNAMIC_VALUES.forEach((inlineDynamic) => { + it(`should update ${InlineDynamic[inlineDynamic]}`, () => { const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ elementDef( @@ -152,7 +134,10 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { [BindingType.ElementAttribute, 'a2', SecurityContext.NONE] ]), ], - config.update)); + (view) => { + setCurrentNode(view, 0); + checkNodeInlineOrDynamic(inlineDynamic, ['v1', 'v2']); + })); checkAndUpdateView(view); @@ -164,23 +149,18 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { }); describe('change classes', () => { - [{ - name: 'inline', - updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, true, true) - }, - { - name: 'dynamic', - updater: (updater: NodeUpdater, view: ViewData) => - updater.checkDynamic(view, 0, [true, true]) - }].forEach((config) => { - it(`should update ${config.name}`, () => { + INLINE_DYNAMIC_VALUES.forEach((inlineDynamic) => { + it(`should update ${InlineDynamic[inlineDynamic]}`, () => { const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ elementDef( NodeFlags.None, null, 0, 'div', null, [[BindingType.ElementClass, 'c1'], [BindingType.ElementClass, 'c2']]), ], - config.updater)); + (view) => { + setCurrentNode(view, 0); + checkNodeInlineOrDynamic(inlineDynamic, [true, true]); + })); checkAndUpdateView(view); @@ -192,16 +172,8 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { }); describe('change styles', () => { - [{ - name: 'inline', - update: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 10, 'red') - }, - { - name: 'dynamic', - update: (updater: NodeUpdater, view: ViewData) => - updater.checkDynamic(view, 0, [10, 'red']) - }].forEach((config) => { - it(`should update ${config.name}`, () => { + INLINE_DYNAMIC_VALUES.forEach((inlineDynamic) => { + it(`should update ${InlineDynamic[inlineDynamic]}`, () => { const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ elementDef( @@ -211,7 +183,10 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { [BindingType.ElementStyle, 'color', null] ]), ], - config.update)); + (view) => { + setCurrentNode(view, 0); + checkNodeInlineOrDynamic(inlineDynamic, [10, 'red']); + })); checkAndUpdateView(view); @@ -346,6 +321,24 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { expect(preventDefaultSpy).toHaveBeenCalled(); }); + it('should report debug info on event errors', () => { + const addListenerSpy = spyOn(HTMLElement.prototype, 'addEventListener').and.callThrough(); + const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef( + [elementDef(NodeFlags.None, null, 0, 'button', null, null, ['click'])], null, + () => { throw new Error('Test'); })); + + let err: any; + try { + addListenerSpy.calls.mostRecent().args[1]('SomeEvent'); + } catch (e) { + err = e; + } + expect(err).toBeTruthy(); + expect(err.message).toBe('Test'); + const debugCtx = err.context; + expect(debugCtx.view).toBe(view); + expect(debugCtx.nodeIndex).toBe(0); + }); }); } }); diff --git a/modules/@angular/core/test/view/embedded_view_spec.ts b/modules/@angular/core/test/view/embedded_view_spec.ts index b64a66d1be..6fd3e101aa 100644 --- a/modules/@angular/core/test/view/embedded_view_spec.ts +++ b/modules/@angular/core/test/view/embedded_view_spec.ts @@ -7,7 +7,7 @@ */ import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core'; -import {BindingType, DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, attachEmbeddedView, checkAndUpdateView, checkNoChangesView, createEmbeddedView, createRootView, destroyView, detachEmbeddedView, elementDef, providerDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; +import {BindingType, DefaultServices, NodeDef, NodeFlags, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, attachEmbeddedView, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createEmbeddedView, createRootView, destroyView, detachEmbeddedView, elementDef, providerDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index'; import {inject} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; @@ -45,7 +45,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { function createAndGetRootNodes( viewDef: ViewDefinition, context: any = null): {rootNodes: any[], view: ViewData} { - const view = createRootView(services, viewDef, context); + const view = createRootView(services, () => viewDef, context); const rootNodes = rootRenderNodes(view); return {rootNodes, view}; } @@ -116,8 +116,10 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { it('should dirty check embedded views', () => { let childValue = 'v1'; - const update = jasmine.createSpy('updater').and.callFake( - (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, childValue)); + const update = jasmine.createSpy('updater').and.callFake((view: ViewData) => { + setCurrentNode(view, 0); + checkNodeInline(childValue); + }); const {view: parentView, rootNodes} = createAndGetRootNodes(compViewDef([ elementDef(NodeFlags.None, null, 1, 'div'), @@ -137,14 +139,12 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { checkAndUpdateView(parentView); - expect(update).toHaveBeenCalled(); - expect(update.calls.mostRecent().args[1]).toBe(childView0); + expect(update).toHaveBeenCalledWith(childView0); update.calls.reset(); checkNoChangesView(parentView); - expect(update).toHaveBeenCalled(); - expect(update.calls.mostRecent().args[1]).toBe(childView0); + expect(update).toHaveBeenCalledWith(childView0); childValue = 'v2'; expect(() => checkNoChangesView(parentView)) diff --git a/modules/@angular/core/test/view/helper.ts b/modules/@angular/core/test/view/helper.ts index ec5dd1f741..bd62e13324 100644 --- a/modules/@angular/core/test/view/helper.ts +++ b/modules/@angular/core/test/view/helper.ts @@ -7,7 +7,7 @@ */ import {RootRenderer} from '@angular/core'; -import {NodeUpdater, ViewData} from '@angular/core/src/view/index'; +import {checkNodeDynamic, checkNodeInline} from '@angular/core/src/view/index'; import {TestBed} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; @@ -43,13 +43,11 @@ export enum InlineDynamic { export const INLINE_DYNAMIC_VALUES = [InlineDynamic.Inline, InlineDynamic.Dynamic]; -export function callUpdater( - updater: NodeUpdater, inlineDynamic: InlineDynamic, view: ViewData, nodeIndex: number, - values: any[]): any { +export function checkNodeInlineOrDynamic(inlineDynamic: InlineDynamic, values: any[]): any { switch (inlineDynamic) { case InlineDynamic.Inline: - return (updater.checkInline)(view, nodeIndex, ...values); + return (checkNodeInline)(...values); case InlineDynamic.Dynamic: - return updater.checkDynamic(view, nodeIndex, values); + return checkNodeDynamic(values); } } diff --git a/modules/@angular/core/test/view/provider_spec.ts b/modules/@angular/core/test/view/provider_spec.ts index 2b2a8d527d..103a3ee82b 100644 --- a/modules/@angular/core/test/view/provider_spec.ts +++ b/modules/@angular/core/test/view/provider_spec.ts @@ -6,12 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, ElementRef, EventEmitter, OnChanges, OnDestroy, OnInit, RenderComponentType, Renderer, RootRenderer, Sanitizer, SecurityContext, SimpleChange, TemplateRef, ViewContainerRef, ViewEncapsulation} from '@angular/core'; -import {BindingType, DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, destroyView, elementDef, providerDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; +import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, ElementRef, EventEmitter, OnChanges, OnDestroy, OnInit, RenderComponentType, Renderer, RootRenderer, Sanitizer, SecurityContext, SimpleChange, TemplateRef, ViewContainerRef, ViewEncapsulation, getDebugNode} from '@angular/core'; +import {BindingType, DebugContext, DefaultServices, NodeDef, NodeFlags, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, asProviderData, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createRootView, destroyView, elementDef, providerDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index'; import {inject} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; -import {isBrowser, setupAndCheckRenderer} from './helper'; +import {INLINE_DYNAMIC_VALUES, InlineDynamic, checkNodeInlineOrDynamic, isBrowser, setupAndCheckRenderer} from './helper'; export function main() { if (isBrowser()) { @@ -44,7 +44,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { } function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} { - const view = createRootView(services, viewDef); + const view = createRootView(services, () => viewDef); const rootNodes = rootRenderNodes(view); return {rootNodes, view}; } @@ -64,6 +64,28 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { expect(instances.length).toBe(1); }); + it('should add a DebugContext to errors in provider factories', () => { + class SomeService { + constructor() { throw new Error('Test'); } + } + + let err: any; + try { + createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.None, null, 0, SomeService, []) + ])); + } catch (e) { + err = e; + } + expect(err).toBeTruthy(); + expect(err.message).toBe('Test'); + const debugCtx = err.context; + expect(debugCtx.view).toBeTruthy(); + // errors should point to the already existing element + expect(debugCtx.nodeIndex).toBe(0); + }); + describe('deps', () => { let instance: SomeService; class Dep {} @@ -149,12 +171,12 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { }); it('should inject ElementRef', () => { - createAndGetRootNodes(compViewDef([ + const {view} = createAndGetRootNodes(compViewDef([ elementDef(NodeFlags.None, null, 1, 'span'), providerDef(NodeFlags.None, null, 0, SomeService, [ElementRef]) ])); - expect(getDOM().nodeName(instance.dep.nativeElement).toLowerCase()).toBe('span'); + expect(instance.dep.nativeElement).toBe(asElementData(view, 0).renderElement); }); if (config.directDom) { @@ -181,16 +203,9 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { }); describe('data binding', () => { - [{ - name: 'inline', - update: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 1, 'v1', 'v2') - }, - { - name: 'dynamic', - update: (updater: NodeUpdater, view: ViewData) => - updater.checkDynamic(view, 1, ['v1', 'v2']) - }].forEach((config) => { - it(`should update ${config.name}`, () => { + + INLINE_DYNAMIC_VALUES.forEach((inlineDynamic) => { + it(`should update ${InlineDynamic[inlineDynamic]}`, () => { let instance: SomeService; class SomeService { @@ -204,36 +219,22 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { elementDef(NodeFlags.None, null, 1, 'span'), providerDef(NodeFlags.None, null, 0, SomeService, [], {a: [0, 'a'], b: [1, 'b']}) ], - config.update)); + (view) => { + setCurrentNode(view, 1); + checkNodeInlineOrDynamic(inlineDynamic, ['v1', 'v2']); + })); checkAndUpdateView(view); expect(instance.a).toBe('v1'); expect(instance.b).toBe('v2'); + + if (!config.directDom) { + const el = rootNodes[0]; + expect(getDOM().getAttribute(el, 'ng-reflect-a')).toBe('v1'); + } }); }); - - it('should checkNoChanges', () => { - class SomeService { - a: any; - } - - let propValue = 'v1'; - const {view, rootNodes} = createAndGetRootNodes(compViewDef( - [ - elementDef(NodeFlags.None, null, 1, 'span'), - providerDef(NodeFlags.None, null, 0, SomeService, [], {a: [0, 'a']}) - ], - (updater, view) => updater.checkInline(view, 1, propValue))); - - checkAndUpdateView(view); - checkNoChangesView(view); - - propValue = 'v2'; - expect(() => checkNoChangesView(view)) - .toThrowError( - `Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`); - }); }); describe('outputs', () => { @@ -268,6 +269,34 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { destroyView(view); expect(unsubscribeSpy).toHaveBeenCalled(); }); + + it('should report debug info on event errors', () => { + let emitter = new EventEmitter(); + + class SomeService { + emitter = emitter; + } + + const {view, rootNodes} = createAndGetRootNodes(compViewDef( + [ + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef( + NodeFlags.None, null, 0, SomeService, [], null, {emitter: 'someEventName'}) + ], + null, () => { throw new Error('Test'); })); + + let err: any; + try { + emitter.emit('someEventInstance'); + } catch (e) { + err = e; + } + expect(err).toBeTruthy(); + const debugCtx = err.context; + expect(debugCtx.view).toBe(view); + // events are emitted with the index of the element, not the index of the provider. + expect(debugCtx.nodeIndex).toBe(0); + }); }); describe('lifecycle hooks', () => { @@ -301,8 +330,10 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { providerDef(allFlags, null, 0, SomeService, [], {a: [0, 'a']}) ], (updater) => { - updater.checkInline(view, 1, 'someValue'); - updater.checkInline(view, 3, 'someValue'); + setCurrentNode(view, 1); + checkNodeInline('someValue'); + setCurrentNode(view, 3); + checkNodeInline('someValue'); })); checkAndUpdateView(view); @@ -357,7 +388,10 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { elementDef(NodeFlags.None, null, 1, 'span'), providerDef(NodeFlags.OnChanges, null, 0, SomeService, [], {a: [0, 'nonMinifiedA']}) ], - (updater) => updater.checkInline(view, 1, currValue))); + (updater) => { + setCurrentNode(view, 1); + checkNodeInline(currValue); + })); checkAndUpdateView(view); expect(changesLog).toEqual([new SimpleChange(undefined, 'v1', true)]); @@ -367,6 +401,52 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { checkAndUpdateView(view); expect(changesLog).toEqual([new SimpleChange('v1', 'v2', false)]); }); + + it('should add a DebugContext to errors in provider afterXXX lifecycles', () => { + class SomeService implements AfterContentChecked { + ngAfterContentChecked() { throw new Error('Test'); } + } + + const {view, rootNodes} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.AfterContentChecked, null, 0, SomeService, [], {a: [0, 'a']}), + ])); + + let err: any; + try { + checkAndUpdateView(view); + } catch (e) { + err = e; + } + expect(err).toBeTruthy(); + expect(err.message).toBe('Test'); + const debugCtx = err.context; + expect(debugCtx.view).toBe(view); + expect(debugCtx.nodeIndex).toBe(1); + }); + + it('should add a DebugContext to errors in destroyView', () => { + class SomeService implements OnDestroy { + ngOnDestroy() { throw new Error('Test'); } + } + + const {view, rootNodes} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.OnDestroy, null, 0, SomeService, [], {a: [0, 'a']}), + ])); + + let err: any; + try { + destroyView(view); + } catch (e) { + err = e; + } + expect(err).toBeTruthy(); + expect(err.message).toBe('Test'); + const debugCtx = err.context; + expect(debugCtx.view).toBe(view); + expect(debugCtx.nodeIndex).toBe(1); + }); }); }); } diff --git a/modules/@angular/core/test/view/pure_expression_spec.ts b/modules/@angular/core/test/view/pure_expression_spec.ts index f601dfbb72..73ce8c5a45 100644 --- a/modules/@angular/core/test/view/pure_expression_spec.ts +++ b/modules/@angular/core/test/view/pure_expression_spec.ts @@ -7,10 +7,10 @@ */ import {PipeTransform, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core'; -import {DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asProviderData, checkAndUpdateView, checkNoChangesView, createRootView, elementDef, providerDef, pureArrayDef, pureObjectDef, purePipeDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; +import {DefaultServices, NodeDef, NodeFlags, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asProviderData, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createRootView, elementDef, providerDef, pureArrayDef, pureObjectDef, purePipeDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index'; import {inject} from '@angular/core/testing'; -import {INLINE_DYNAMIC_VALUES, InlineDynamic, callUpdater} from './helper'; +import {INLINE_DYNAMIC_VALUES, InlineDynamic, checkNodeInlineOrDynamic} from './helper'; export function main() { describe(`View Pure Expressions`, () => { @@ -30,7 +30,7 @@ export function main() { } function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} { - const view = createRootView(services, viewDef); + const view = createRootView(services, () => viewDef); const rootNodes = rootRenderNodes(view); return {rootNodes, view}; } @@ -48,10 +48,11 @@ export function main() { elementDef(NodeFlags.None, null, 2, 'span'), pureArrayDef(2), providerDef(NodeFlags.None, null, 0, Service, [], {data: [0, 'data']}) ], - (updater, view) => { - callUpdater( - updater, inlineDynamic, view, 2, - [callUpdater(updater, inlineDynamic, view, 1, values)]); + (view) => { + setCurrentNode(view, 1); + const pureValue = checkNodeInlineOrDynamic(inlineDynamic, values); + setCurrentNode(view, 2); + checkNodeInlineOrDynamic(inlineDynamic, [pureValue]); })); const service = asProviderData(view, 2).instance; @@ -82,10 +83,11 @@ export function main() { elementDef(NodeFlags.None, null, 2, 'span'), pureObjectDef(['a', 'b']), providerDef(NodeFlags.None, null, 0, Service, [], {data: [0, 'data']}) ], - (updater, view) => { - callUpdater( - updater, inlineDynamic, view, 2, - [callUpdater(updater, inlineDynamic, view, 1, values)]); + (view) => { + setCurrentNode(view, 1); + const pureValue = checkNodeInlineOrDynamic(inlineDynamic, values); + setCurrentNode(view, 2); + checkNodeInlineOrDynamic(inlineDynamic, [pureValue]); })); const service = asProviderData(view, 2).instance; @@ -121,10 +123,11 @@ export function main() { providerDef(NodeFlags.None, null, 0, SomePipe, []), purePipeDef(SomePipe, 2), providerDef(NodeFlags.None, null, 0, Service, [], {data: [0, 'data']}) ], - (updater, view) => { - callUpdater( - updater, inlineDynamic, view, 3, - [callUpdater(updater, inlineDynamic, view, 2, values)]); + (view) => { + setCurrentNode(view, 2); + const pureValue = checkNodeInlineOrDynamic(inlineDynamic, values); + setCurrentNode(view, 3); + checkNodeInlineOrDynamic(inlineDynamic, [pureValue]); })); const service = asProviderData(view, 3).instance; diff --git a/modules/@angular/core/test/view/query_spec.ts b/modules/@angular/core/test/view/query_spec.ts index bd49540d23..58f1b17366 100644 --- a/modules/@angular/core/test/view/query_spec.ts +++ b/modules/@angular/core/test/view/query_spec.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {ElementRef, QueryList, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, TemplateRef, ViewContainerRef, ViewEncapsulation} from '@angular/core'; -import {BindingType, DefaultServices, NodeDef, NodeFlags, NodeUpdater, QueryBindingType, QueryValueType, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, asProviderData, attachEmbeddedView, checkAndUpdateView, checkNoChangesView, createEmbeddedView, createRootView, destroyView, detachEmbeddedView, elementDef, providerDef, queryDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; +import {ElementRef, QueryList, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, TemplateRef, ViewContainerRef, ViewEncapsulation, getDebugNode} from '@angular/core'; +import {BindingType, DebugContext, DefaultServices, NodeDef, NodeFlags, QueryBindingType, QueryValueType, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, asProviderData, attachEmbeddedView, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createEmbeddedView, createRootView, destroyView, detachEmbeddedView, elementDef, providerDef, queryDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index'; import {inject} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; @@ -34,7 +34,7 @@ export function main() { function createAndGetRootNodes( viewDef: ViewDefinition, context: any = null): {rootNodes: any[], view: ViewData} { - const view = createRootView(services, viewDef, context); + const view = createRootView(services, () => viewDef, context); const rootNodes = rootRenderNodes(view); return {rootNodes, view}; } @@ -197,30 +197,6 @@ export function main() { expect(qs2.a.length).toBe(0); }); - it('should checkNoChanges', () => { - const {view} = createAndGetRootNodes(compViewDef([ - elementDef(NodeFlags.None, null, 4, 'div'), - ...contentQueryProviders(), - anchorDef( - NodeFlags.HasEmbeddedViews, null, 1, viewDef( - ViewFlags.None, - [ - elementDef(NodeFlags.None, null, 1, 'div'), - aServiceProvider(), - ])), - ])); - - checkAndUpdateView(view); - checkNoChangesView(view); - - const childView = createEmbeddedView(view, view.def.nodes[3]); - attachEmbeddedView(asElementData(view, 3), 0, childView); - - expect(() => checkNoChangesView(view)) - .toThrowError( - `Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'.`); - }); - it('should update content queries if embedded views are added or removed', () => { const {view} = createAndGetRootNodes(compViewDef([ elementDef(NodeFlags.None, null, 3, 'div'), @@ -383,5 +359,67 @@ export function main() { expect(qs.a.createEmbeddedView).toBeTruthy(); }); }); + + describe('general binding behavior', () => { + it('should checkNoChanges', () => { + const {view} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 4, 'div'), + ...contentQueryProviders(), + anchorDef( + NodeFlags.HasEmbeddedViews, null, 1, viewDef( + ViewFlags.None, + [ + elementDef(NodeFlags.None, null, 1, 'div'), + aServiceProvider(), + ])), + ])); + + checkAndUpdateView(view); + checkNoChangesView(view); + + const childView = createEmbeddedView(view, view.def.nodes[3]); + attachEmbeddedView(asElementData(view, 3), 0, childView); + + let err: any; + try { + checkNoChangesView(view); + } catch (e) { + err = e; + } + expect(err).toBeTruthy(); + expect(err.message) + .toBe( + `Expression has changed after it was checked. Previous value: 'Query query1 not dirty'. Current value: 'Query query1 dirty'.`); + const debugCtx = err.context; + expect(debugCtx.view).toBe(view); + expect(debugCtx.nodeIndex).toBe(2); + }); + + it('should report debug info on binding errors', () => { + class QueryService { + set a(value: any) { throw new Error('Test'); } + } + + const {view} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 3, 'div'), + providerDef(NodeFlags.None, null, 1, QueryService, []), + queryDef(NodeFlags.HasContentQuery, 'query1', {'a': QueryBindingType.All}), + aServiceProvider(), + ])); + + + let err: any; + try { + checkAndUpdateView(view); + } catch (e) { + err = e; + } + expect(err).toBeTruthy(); + expect(err.message).toBe('Test'); + const debugCtx = err.context; + expect(debugCtx.view).toBe(view); + expect(debugCtx.nodeIndex).toBe(2); + }); + }); }); } \ No newline at end of file diff --git a/modules/@angular/core/test/view/services_spec.ts b/modules/@angular/core/test/view/services_spec.ts new file mode 100644 index 0000000000..2dc01aa206 --- /dev/null +++ b/modules/@angular/core/test/view/services_spec.ts @@ -0,0 +1,99 @@ +/** + * @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 {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, getDebugNode} from '@angular/core'; +import {DebugContext, DefaultServices, NodeDef, NodeFlags, QueryValueType, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, asProviderData, asTextData, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createRootView, elementDef, providerDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index'; +import {inject} from '@angular/core/testing'; +import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; + +import {isBrowser, setupAndCheckRenderer} from './helper'; + +export function main() { + describe('View Services', () => { + let services: Services; + let renderComponentType: RenderComponentType; + + beforeEach( + inject([RootRenderer, Sanitizer], (rootRenderer: RootRenderer, sanitizer: Sanitizer) => { + services = new DefaultServices(rootRenderer, sanitizer); + renderComponentType = + new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {}); + })); + + function compViewDef( + nodes: NodeDef[], update?: ViewUpdateFn, handleEvent?: ViewHandleEventFn): ViewDefinition { + return viewDef(ViewFlags.None, nodes, update, handleEvent, renderComponentType); + } + + function createAndGetRootNodes( + viewDef: ViewDefinition, context: any = null): {rootNodes: any[], view: ViewData} { + const view = createRootView(services, () => viewDef, context); + const rootNodes = rootRenderNodes(view); + return {rootNodes, view}; + } + + describe('DebugContext', () => { + class AComp {} + + class AService {} + + function createViewWithData() { + const {view} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 1, 'div'), + providerDef( + NodeFlags.None, null, 0, AComp, [], null, null, + () => compViewDef([ + elementDef(NodeFlags.None, [['#ref', QueryValueType.ElementRef]], 2, 'span'), + providerDef(NodeFlags.None, null, 0, AService, []), textDef(['a']) + ])), + ])); + return view; + } + + it('should provide data for elements', () => { + const view = createViewWithData(); + const compView = asProviderData(view, 1).componentView; + + const debugCtx = view.services.createDebugContext(compView, 0); + + expect(debugCtx.componentRenderElement).toBe(asElementData(view, 0).renderElement); + expect(debugCtx.renderNode).toBe(asElementData(compView, 0).renderElement); + expect(debugCtx.injector.get(AComp)).toBe(compView.component); + expect(debugCtx.component).toBe(compView.component); + expect(debugCtx.context).toBe(compView.context); + expect(debugCtx.providerTokens).toEqual([AService]); + expect(debugCtx.source).toBeTruthy(); + expect(debugCtx.references['ref'].nativeElement) + .toBe(asElementData(compView, 0).renderElement); + }); + + it('should provide data for text nodes', () => { + const view = createViewWithData(); + const compView = asProviderData(view, 1).componentView; + + const debugCtx = view.services.createDebugContext(compView, 2); + + expect(debugCtx.componentRenderElement).toBe(asElementData(view, 0).renderElement); + expect(debugCtx.renderNode).toBe(asTextData(compView, 2).renderText); + expect(debugCtx.injector.get(AComp)).toBe(compView.component); + expect(debugCtx.component).toBe(compView.component); + expect(debugCtx.context).toBe(compView.context); + expect(debugCtx.source).toBeTruthy(); + }); + + it('should provide data for other nodes based on the nearest element parent', () => { + const view = createViewWithData(); + const compView = asProviderData(view, 1).componentView; + + const debugCtx = view.services.createDebugContext(compView, 1); + + expect(debugCtx.renderNode).toBe(asElementData(compView, 0).renderElement); + }); + }); + }); +} diff --git a/modules/@angular/core/test/view/text_spec.ts b/modules/@angular/core/test/view/text_spec.ts index af2118c7ad..b7070550d8 100644 --- a/modules/@angular/core/test/view/text_spec.ts +++ b/modules/@angular/core/test/view/text_spec.ts @@ -6,12 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core'; -import {DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; +import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, getDebugNode} from '@angular/core'; +import {DebugContext, DefaultServices, NodeDef, NodeFlags, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asTextData, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createRootView, elementDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index'; import {inject} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; -import {isBrowser, setupAndCheckRenderer} from './helper'; +import {INLINE_DYNAMIC_VALUES, InlineDynamic, checkNodeInlineOrDynamic, isBrowser, setupAndCheckRenderer} from './helper'; export function main() { if (isBrowser()) { @@ -39,8 +39,9 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { return viewDef(config.viewFlags, nodes, update, handleEvent, renderComponentType); } - function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} { - const view = createRootView(services, viewDef); + function createAndGetRootNodes( + viewDef: ViewDefinition, context?: any): {rootNodes: any[], view: ViewData} { + const view = createRootView(services, () => viewDef, context); const rootNodes = rootRenderNodes(view); return {rootNodes, view}; } @@ -67,41 +68,28 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { const textNode = getDOM().firstChild(rootNodes[0]); expect(getDOM().getText(textNode)).toBe('a'); }); - }); - it('should checkNoChanges', () => { - let textValue = 'v1'; - const {view, rootNodes} = createAndGetRootNodes(compViewDef( - [ - textDef(['', '']), - ], - (updater, view) => updater.checkInline(view, 0, textValue))); - - checkAndUpdateView(view); - checkNoChangesView(view); - - textValue = 'v2'; - expect(() => checkNoChangesView(view)) - .toThrowError( - `Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`); + if (!config.directDom) { + it('should add debug information to the renderer', () => { + const someContext = new Object(); + const {view, rootNodes} = + createAndGetRootNodes(compViewDef([textDef(['a'])]), someContext); + expect(getDebugNode(rootNodes[0]).nativeNode).toBe(asTextData(view, 0).renderText); + }); + } }); describe('change text', () => { - [{ - name: 'inline', - update: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 'a', 'b') - }, - { - name: 'dynamic', - update: (updater: NodeUpdater, view: ViewData) => - updater.checkDynamic(view, 0, ['a', 'b']) - }].forEach((config) => { - it(`should update ${config.name}`, () => { + INLINE_DYNAMIC_VALUES.forEach((inlineDynamic) => { + it(`should update ${InlineDynamic[inlineDynamic]}`, () => { const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ textDef(['0', '1', '2']), ], - config.update)); + (view: ViewData) => { + setCurrentNode(view, 0); + checkNodeInlineOrDynamic(inlineDynamic, ['a', 'b']); + })); checkAndUpdateView(view); diff --git a/modules/@angular/core/test/view/view_def_spec.ts b/modules/@angular/core/test/view/view_def_spec.ts index 142203b488..f6d1184e0c 100644 --- a/modules/@angular/core/test/view/view_def_spec.ts +++ b/modules/@angular/core/test/view/view_def_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {NodeFlags, NodeUpdater, QueryValueType, ViewData, ViewDefinition, ViewFlags, anchorDef, checkAndUpdateView, checkNoChangesView, elementDef, providerDef, textDef, viewDef} from '@angular/core/src/view/index'; +import {NodeFlags, QueryValueType, ViewData, ViewDefinition, ViewFlags, anchorDef, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, elementDef, providerDef, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index'; export function main() { describe('viewDef', () => { diff --git a/modules/benchmarks/src/tree/ng2_next/index.ts b/modules/benchmarks/src/tree/ng2_next/index.ts index 6bb0b5693d..7585c1dfaf 100644 --- a/modules/benchmarks/src/tree/ng2_next/index.ts +++ b/modules/benchmarks/src/tree/ng2_next/index.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationRef, NgModuleRef} from '@angular/core'; +import {ApplicationRef, NgModuleRef, enableProdMode} from '@angular/core'; import {bindAction, profile} from '../../util'; import {buildTree, emptyTree} from '../util'; @@ -40,6 +40,7 @@ export function main() { const numberOfChecksEl = document.getElementById('numberOfChecks'); + enableProdMode(); appMod = new AppModule(); appMod.bootstrap(); tree = appMod.rootComp; diff --git a/modules/benchmarks/src/tree/ng2_next/tree.ts b/modules/benchmarks/src/tree/ng2_next/tree.ts index 45dd4ad68d..527fe180bb 100644 --- a/modules/benchmarks/src/tree/ng2_next/tree.ts +++ b/modules/benchmarks/src/tree/ng2_next/tree.ts @@ -8,7 +8,7 @@ import {NgIf} from '@angular/common'; import {Component, NgModule, TemplateRef, ViewContainerRef, ViewEncapsulation} from '@angular/core'; -import {BindingType, DefaultServices, NodeFlags, NodeUpdater, ViewData, ViewDefinition, ViewFlags, anchorDef, asElementData, asProviderData, checkAndUpdateView, createRootView, elementDef, providerDef, textDef, viewDef} from '@angular/core/src/view/index'; +import {BindingType, DefaultServices, NodeFlags, ViewData, ViewDefinition, ViewFlags, anchorDef, asElementData, asProviderData, checkAndUpdateView, checkNodeInline, createRootView, elementDef, providerDef, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index'; import {DomSanitizer, DomSanitizerImpl, SafeStyle} from '@angular/platform-browser/src/security/dom_sanitization_service'; import {TreeNode, emptyTree} from '../util'; @@ -23,58 +23,66 @@ export class TreeComponent { let viewFlags = ViewFlags.DirectDom; -const TreeComponent_Host: ViewDefinition = viewDef(viewFlags, [ - elementDef(NodeFlags.None, null, 1, 'tree'), - providerDef(NodeFlags.None, null, 0, TreeComponent, [], null, null, () => TreeComponent_0), -]); +function TreeComponent_Host(): ViewDefinition { + return viewDef(viewFlags, [ + elementDef(NodeFlags.None, null, 1, 'tree'), + providerDef(NodeFlags.None, null, 0, TreeComponent, [], null, null, TreeComponent_0), + ]); +} -const TreeComponent_1: ViewDefinition = viewDef( - viewFlags, - [ - elementDef(NodeFlags.None, null, 1, 'tree'), - providerDef( - NodeFlags.None, null, 0, TreeComponent, [], {data: [0, 'data']}, null, - () => TreeComponent_0), - ], - (updater: NodeUpdater, view: ViewData) => { - const cmp = view.component; - updater.checkInline(view, 1, cmp.data.left); - }); +function TreeComponent_0(): ViewDefinition { + const TreeComponent_1: ViewDefinition = viewDef( + viewFlags, + [ + elementDef(NodeFlags.None, null, 1, 'tree'), + providerDef( + NodeFlags.None, null, 0, TreeComponent, [], {data: [0, 'data']}, null, TreeComponent_0), + ], + (view: ViewData) => { + const cmp = view.component; + setCurrentNode(view, 1); + checkNodeInline(cmp.data.left); + }); -const TreeComponent_2: ViewDefinition = viewDef( - viewFlags, - [ - elementDef(NodeFlags.None, null, 1, 'tree'), - providerDef( - NodeFlags.None, null, 0, TreeComponent, [], {data: [0, 'data']}, null, - () => TreeComponent_0), - ], - (updater: NodeUpdater, view: ViewData) => { - const cmp = view.component; - updater.checkInline(view, 1, cmp.data.right); - }); + const TreeComponent_2: ViewDefinition = viewDef( + viewFlags, + [ + elementDef(NodeFlags.None, null, 1, 'tree'), + providerDef( + NodeFlags.None, null, 0, TreeComponent, [], {data: [0, 'data']}, null, TreeComponent_0), + ], + (view: ViewData) => { + const cmp = view.component; + setCurrentNode(view, 1); + checkNodeInline(cmp.data.right); + }); -const TreeComponent_0: ViewDefinition = viewDef( - viewFlags, - [ - elementDef( - NodeFlags.None, null, 1, 'span', null, - [[BindingType.ElementStyle, 'backgroundColor', null]]), - textDef([' ', ' ']), - anchorDef(NodeFlags.HasEmbeddedViews, null, 1, TreeComponent_1), - providerDef( - NodeFlags.None, null, 0, NgIf, [ViewContainerRef, TemplateRef], {ngIf: [0, 'ngIf']}), - anchorDef(NodeFlags.HasEmbeddedViews, null, 1, TreeComponent_2), - providerDef( - NodeFlags.None, null, 0, NgIf, [ViewContainerRef, TemplateRef], {ngIf: [0, 'ngIf']}), - ], - (updater: NodeUpdater, view: ViewData) => { - const cmp = view.component; - updater.checkInline(view, 0, cmp.bgColor); - updater.checkInline(view, 1, cmp.data.value); - updater.checkInline(view, 3, cmp.data.left != null); - updater.checkInline(view, 5, cmp.data.right != null); - }); + return viewDef( + viewFlags, + [ + elementDef( + NodeFlags.None, null, 1, 'span', null, + [[BindingType.ElementStyle, 'backgroundColor', null]]), + textDef([' ', ' ']), + anchorDef(NodeFlags.HasEmbeddedViews, null, 1, TreeComponent_1), + providerDef( + NodeFlags.None, null, 0, NgIf, [ViewContainerRef, TemplateRef], {ngIf: [0, 'ngIf']}), + anchorDef(NodeFlags.HasEmbeddedViews, null, 1, TreeComponent_2), + providerDef( + NodeFlags.None, null, 0, NgIf, [ViewContainerRef, TemplateRef], {ngIf: [0, 'ngIf']}), + ], + (view: ViewData) => { + const cmp = view.component; + setCurrentNode(view, 0); + checkNodeInline(cmp.bgColor); + setCurrentNode(view, 1); + checkNodeInline(cmp.data.value); + setCurrentNode(view, 3); + checkNodeInline(cmp.data.left != null); + setCurrentNode(view, 5); + checkNodeInline(cmp.data.right != null); + }); +} export class AppModule { public rootComp: TreeComponent;