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:
parent
c48dd76f5c
commit
52b21275f4
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue