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
This commit is contained in:
Tobias Bosch 2017-01-26 17:07:37 -08:00 committed by Miško Hevery
parent c48dd76f5c
commit 52b21275f4
23 changed files with 1007 additions and 433 deletions

View File

@ -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 {

View File

@ -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;
}

View File

@ -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';

View File

@ -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<any, string>();
@ -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

View File

@ -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 = <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 = <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;
}
}

View File

@ -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<any> {
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<any> {
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);
}
}
}

View File

@ -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) {

View File

@ -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<any>;
// 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;
}

View File

@ -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;
}
}
}
export function isComponentView(view: ViewData): boolean {
return view.component === view.context && !!view.parent;
}
const VIEW_DEFINITION_CACHE = new WeakMap<any, ViewDefinition>();
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<A, R>(action: EntryAction, fn: (arg: A) => R): (arg: A) => R {
return <any>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);
}
}

View File

@ -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);

View File

@ -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);
});
}
});
});
}

View File

@ -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))

View File

@ -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 = <DebugContext>err.context;
expect(debugCtx.view).toBe(view);
expect(debugCtx.nodeIndex).toBe(0);
});
});
}
});

View File

@ -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))

View File

@ -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 (<any>updater.checkInline)(view, nodeIndex, ...values);
return (<any>checkNodeInline)(...values);
case InlineDynamic.Dynamic:
return updater.checkDynamic(view, nodeIndex, values);
return checkNodeDynamic(values);
}
}

View File

@ -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 = <DebugContext>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<any>();
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 = <DebugContext>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 = <DebugContext>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 = <DebugContext>err.context;
expect(debugCtx.view).toBe(view);
expect(debugCtx.nodeIndex).toBe(1);
});
});
});
}

View File

@ -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;

View File

@ -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 = <DebugContext>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 = <DebugContext>err.context;
expect(debugCtx.view).toBe(view);
expect(debugCtx.nodeIndex).toBe(2);
});
});
});
}

View File

@ -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);
});
});
});
}

View File

@ -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);

View File

@ -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', () => {

View File

@ -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;

View File

@ -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;