feat(core): add pure expression support to view engine

Part of #14013
This commit is contained in:
Tobias Bosch 2017-01-20 09:21:09 -08:00 committed by Alex Rickabaugh
parent 0adb97bffb
commit 65417374f1
10 changed files with 508 additions and 101 deletions

View File

@ -1,52 +0,0 @@
/**
* @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 {NodeData, NodeDef, NodeFlags, NodeType, ViewData, ViewDefinition} from './types';
export function anchorDef(
flags: NodeFlags, childCount: number, template?: ViewDefinition): NodeDef {
return {
type: NodeType.Anchor,
// will bet set by the view definition
index: undefined,
reverseChildIndex: undefined,
parent: undefined,
childFlags: undefined,
bindingIndex: undefined,
disposableIndex: undefined,
providerIndices: undefined,
// regular values
flags,
childCount,
bindings: [],
disposableCount: 0,
element: undefined,
provider: undefined,
text: undefined,
component: undefined, template
};
}
export function createAnchor(view: ViewData, renderHost: any, def: NodeDef): NodeData {
const parentNode = def.parent != null ? view.nodes[def.parent].renderNode : renderHost;
let renderNode: any;
if (view.renderer) {
renderNode = view.renderer.createTemplateAnchor(parentNode);
} else {
renderNode = document.createComment('');
if (parentNode) {
parentNode.appendChild(renderNode);
}
}
return {
renderNode,
provider: undefined,
embeddedViews: (def.flags & NodeFlags.HasEmbeddedViews) ? [] : undefined,
componentView: undefined
};
}

View File

@ -8,9 +8,33 @@
import {SecurityContext} from '../security'; import {SecurityContext} from '../security';
import {BindingDef, BindingType, DisposableFn, ElementOutputDef, NodeData, NodeDef, NodeFlags, NodeType, ViewData, ViewFlags} from './types'; import {BindingDef, BindingType, DisposableFn, ElementOutputDef, NodeData, NodeDef, NodeFlags, NodeType, ViewData, ViewDefinition, ViewFlags} from './types';
import {checkAndUpdateBinding, setBindingDebugInfo} from './util'; import {checkAndUpdateBinding, setBindingDebugInfo} from './util';
export function anchorDef(
flags: NodeFlags, childCount: number, template?: ViewDefinition): NodeDef {
return {
type: NodeType.Element,
// will bet set by the view definition
index: undefined,
reverseChildIndex: undefined,
parent: undefined,
childFlags: undefined,
bindingIndex: undefined,
disposableIndex: undefined,
providerIndices: undefined,
// regular values
flags,
childCount,
bindings: [],
disposableCount: 0,
element: {name: undefined, attrs: undefined, outputs: [], template},
provider: undefined,
text: undefined,
pureExpression: undefined
};
}
export function elementDef( export function elementDef(
flags: NodeFlags, childCount: number, name: string, fixedAttrs: {[name: string]: string} = {}, flags: NodeFlags, childCount: number, name: string, fixedAttrs: {[name: string]: string} = {},
bindings?: bindings?:
@ -65,11 +89,10 @@ export function elementDef(
childCount, childCount,
bindings: bindingDefs, bindings: bindingDefs,
disposableCount: outputDefs.length, disposableCount: outputDefs.length,
element: {name, attrs: fixedAttrs, outputs: outputDefs}, element: {name, attrs: fixedAttrs, outputs: outputDefs, template: undefined},
provider: undefined, provider: undefined,
text: undefined, text: undefined,
component: undefined, pureExpression: undefined
template: undefined
}; };
} }
@ -78,9 +101,10 @@ export function createElement(view: ViewData, renderHost: any, def: NodeDef): No
const elDef = def.element; const elDef = def.element;
let el: any; let el: any;
if (view.renderer) { if (view.renderer) {
el = view.renderer.createElement(parentNode, elDef.name); el = elDef.name ? view.renderer.createElement(parentNode, elDef.name) :
view.renderer.createTemplateAnchor(parentNode);
} else { } else {
el = document.createElement(elDef.name); el = elDef.name ? document.createElement(elDef.name) : document.createComment('');
if (parentNode) { if (parentNode) {
parentNode.appendChild(el); parentNode.appendChild(el);
} }

View File

@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
export {anchorDef} from './anchor'; export {anchorDef, elementDef} from './element';
export {elementDef} from './element';
export {providerDef} from './provider'; export {providerDef} from './provider';
export {pureArrayDef, pureObjectDef, purePipeDef} from './pure_expression';
export {textDef} from './text'; export {textDef} from './text';
export {checkAndUpdateView, checkNoChangesView, createEmbeddedView, createRootView, destroyView, viewDef} from './view'; export {checkAndUpdateView, checkNoChangesView, createEmbeddedView, createRootView, destroyView, viewDef} from './view';
export {attachEmbeddedView, detachEmbeddedView, rootRenderNodes} from './view_attach'; export {attachEmbeddedView, detachEmbeddedView, rootRenderNodes} from './view_attach';

View File

@ -75,9 +75,9 @@ export function providerDef(
childCount: 0, bindings, childCount: 0, bindings,
disposableCount: outputDefs.length, disposableCount: outputDefs.length,
element: undefined, element: undefined,
provider: {tokenKey: tokenKey(ctor), ctor, deps: depDefs, outputs: outputDefs}, provider: {tokenKey: tokenKey(ctor), ctor, deps: depDefs, outputs: outputDefs, component},
text: undefined, component, text: undefined,
template: undefined pureExpression: undefined
}; };
} }
@ -147,8 +147,7 @@ export function checkAndUpdateProviderInline(
} }
} }
export function checkAndUpdateProviderDynamic( export function checkAndUpdateProviderDynamic(view: ViewData, def: NodeDef, values: any[]) {
view: ViewData, index: number, def: NodeDef, values: any[]) {
const provider = view.nodes[def.index].provider; const provider = view.nodes[def.index].provider;
let changes: SimpleChanges; let changes: SimpleChanges;
for (let i = 0; i < values.length; i++) { for (let i = 0; i < values.length; i++) {

View File

@ -0,0 +1,227 @@
/**
* @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 {resolveDep, tokenKey} from './provider';
import {BindingDef, BindingType, DepDef, DepFlags, NodeData, NodeDef, NodeType, PureExpressionData, PureExpressionType, ViewData} from './types';
import {checkAndUpdateBinding} from './util';
export function purePipeDef(pipeToken: any, argCount: number): NodeDef {
return _pureExpressionDef(
PureExpressionType.Pipe, new Array(argCount),
{token: pipeToken, tokenKey: tokenKey(pipeToken), flags: DepFlags.None});
}
export function pureArrayDef(argCount: number): NodeDef {
return _pureExpressionDef(PureExpressionType.Array, new Array(argCount), undefined);
}
export function pureObjectDef(propertyNames: string[]): NodeDef {
return _pureExpressionDef(PureExpressionType.Object, propertyNames, undefined);
}
function _pureExpressionDef(
type: PureExpressionType, propertyNames: string[], pipeDep: DepDef): NodeDef {
const bindings: BindingDef[] = new Array(propertyNames.length);
for (let i = 0; i < propertyNames.length; i++) {
const prop = propertyNames[i];
bindings[i] = {
type: BindingType.PureExpressionProperty,
name: prop,
nonMinifiedName: prop,
securityContext: undefined,
suffix: undefined
};
}
return {
type: NodeType.PureExpression,
// will bet set by the view definition
index: undefined,
reverseChildIndex: undefined,
parent: undefined,
childFlags: undefined,
bindingIndex: undefined,
disposableIndex: undefined,
providerIndices: undefined,
// regular values
flags: 0,
childCount: 0, bindings,
disposableCount: 0,
element: undefined,
provider: undefined,
text: undefined,
pureExpression: {type, pipeDep}
};
}
export function createPureExpression(view: ViewData, def: NodeDef): NodeData {
const pipe = def.pureExpression.pipeDep ?
resolveDep(view, def.parent, def.pureExpression.pipeDep) :
undefined;
const data: PureExpressionData = {value: undefined, pipe: pipe};
return {
renderNode: undefined,
provider: data,
embeddedViews: undefined,
componentView: undefined
};
}
export function checkAndUpdatePureExpressionInline(
view: ViewData, def: NodeDef, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any,
v7: any, v8: any, v9: any) {
const bindings = def.bindings;
let changed = false;
// Note: fallthrough is intended!
switch (bindings.length) {
case 10:
if (checkAndUpdateBinding(view, def, 9, v9)) changed = true;
case 9:
if (checkAndUpdateBinding(view, def, 8, v8)) changed = true;
case 8:
if (checkAndUpdateBinding(view, def, 7, v7)) changed = true;
case 7:
if (checkAndUpdateBinding(view, def, 6, v6)) changed = true;
case 6:
if (checkAndUpdateBinding(view, def, 5, v5)) changed = true;
case 5:
if (checkAndUpdateBinding(view, def, 4, v4)) changed = true;
case 4:
if (checkAndUpdateBinding(view, def, 3, v3)) changed = true;
case 3:
if (checkAndUpdateBinding(view, def, 2, v2)) changed = true;
case 2:
if (checkAndUpdateBinding(view, def, 1, v1)) changed = true;
case 1:
if (checkAndUpdateBinding(view, def, 0, v0)) changed = true;
}
if (changed) {
const data: PureExpressionData = view.nodes[def.index].provider;
let value: any;
switch (def.pureExpression.type) {
case PureExpressionType.Array:
value = new Array(bindings.length);
// Note: fallthrough is intended!
switch (bindings.length) {
case 10:
value[9] = v9;
case 9:
value[8] = v8;
case 8:
value[7] = v7;
case 7:
value[6] = v6;
case 6:
value[5] = v5;
case 5:
value[4] = v4;
case 4:
value[3] = v3;
case 3:
value[2] = v2;
case 2:
value[1] = v1;
case 1:
value[0] = v0;
}
break;
case PureExpressionType.Object:
value = {};
// Note: fallthrough is intended!
switch (bindings.length) {
case 10:
value[bindings[9].name] = v9;
case 9:
value[bindings[8].name] = v8;
case 8:
value[bindings[7].name] = v7;
case 7:
value[bindings[6].name] = v6;
case 6:
value[bindings[5].name] = v5;
case 5:
value[bindings[4].name] = v4;
case 4:
value[bindings[3].name] = v3;
case 3:
value[bindings[2].name] = v2;
case 2:
value[bindings[1].name] = v1;
case 1:
value[bindings[0].name] = v0;
}
break;
case PureExpressionType.Pipe:
switch (bindings.length) {
case 10:
value = data.pipe.transform(v0, v1, v2, v3, v4, v5, v6, v7, v8, v9);
break;
case 9:
value = data.pipe.transform(v0, v1, v2, v3, v4, v5, v6, v7, v8);
break;
case 8:
value = data.pipe.transform(v0, v1, v2, v3, v4, v5, v6, v7);
break;
case 7:
value = data.pipe.transform(v0, v1, v2, v3, v4, v5, v6);
break;
case 6:
value = data.pipe.transform(v0, v1, v2, v3, v4, v5);
break;
case 5:
value = data.pipe.transform(v0, v1, v2, v3, v4);
break;
case 4:
value = data.pipe.transform(v0, v1, v2, v3);
break;
case 3:
value = data.pipe.transform(v0, v1, v2);
break;
case 2:
value = data.pipe.transform(v0, v1);
break;
case 1:
value = data.pipe.transform(v0);
break;
}
break;
}
data.value = value;
}
}
export function checkAndUpdatePureExpressionDynamic(view: ViewData, def: NodeDef, values: any[]) {
const bindings = def.bindings;
let changed = false;
for (let i = 0; i < values.length; i++) {
// Note: We need to loop over all values, so that
// the old values are updates as well!
if (checkAndUpdateBinding(view, def, i, values[i])) {
changed = true;
}
}
if (changed) {
const data: PureExpressionData = view.nodes[def.index].provider;
let value: any;
switch (def.pureExpression.type) {
case PureExpressionType.Array:
value = values;
break;
case PureExpressionType.Object:
value = {};
for (let i = 0; i < values.length; i++) {
value[bindings[i].name] = values[i];
}
break;
case PureExpressionType.Pipe:
value = data.pipe.transform(values[0], ...values.slice(1));
break;
}
data.value = value;
}
}

View File

@ -39,8 +39,7 @@ export function textDef(constants: string[]): NodeDef {
element: undefined, element: undefined,
provider: undefined, provider: undefined,
text: {prefix: constants[0]}, text: {prefix: constants[0]},
component: undefined, pureExpression: undefined
template: undefined
}; };
} }
@ -124,9 +123,13 @@ export function checkAndUpdateTextInline(
export function checkAndUpdateTextDynamic(view: ViewData, def: NodeDef, values: any[]) { export function checkAndUpdateTextDynamic(view: ViewData, def: NodeDef, values: any[]) {
const bindings = def.bindings; const bindings = def.bindings;
let changed = view.firstChange; let changed = false;
for (let i = 0; i < values.length && !changed; i++) { for (let i = 0; i < values.length; i++) {
changed = changed || checkAndUpdateBinding(view, def, i, values[i]); // Note: We need to loop over all values, so that
// the old values are updates as well!
if (checkAndUpdateBinding(view, def, i, values[i])) {
changed = true;
}
} }
if (changed) { if (changed) {
let value = ''; let value = '';

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {PipeTransform} from '../change_detection/change_detection';
import {TemplateRef} from '../linker/template_ref'; import {TemplateRef} from '../linker/template_ref';
import {ViewContainerRef} from '../linker/view_container_ref'; import {ViewContainerRef} from '../linker/view_container_ref';
import {RenderComponentType, Renderer, RootRenderer} from '../render/api'; import {RenderComponentType, Renderer, RootRenderer} from '../render/api';
@ -42,8 +43,8 @@ export type ViewUpdateFn = (updater: NodeUpdater, view: ViewData) => void;
export interface NodeUpdater { export interface NodeUpdater {
checkInline( checkInline(
view: ViewData, nodeIndex: number, v0?: any, v1?: any, v2?: any, v3?: any, v4?: any, v5?: any, 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; v6?: any, v7?: any, v8?: any, v9?: any): any;
checkDynamic(view: ViewData, nodeIndex: number, values: any[]): void; checkDynamic(view: ViewData, nodeIndex: number, values: any[]): any;
} }
export type ViewHandleEventFn = export type ViewHandleEventFn =
@ -68,24 +69,22 @@ export interface NodeDef {
childCount: number; childCount: number;
/** aggregated NodeFlags for all children **/ /** aggregated NodeFlags for all children **/
childFlags: NodeFlags; childFlags: NodeFlags;
providerIndices: {[tokenKey: string]: number};
bindingIndex: number; bindingIndex: number;
bindings: BindingDef[]; bindings: BindingDef[];
disposableIndex: number; disposableIndex: number;
disposableCount: number; disposableCount: number;
element: ElementDef; element: ElementDef;
providerIndices: {[tokenKey: string]: number};
provider: ProviderDef; provider: ProviderDef;
text: TextDef; text: TextDef;
// closure to allow recursive components pureExpression: PureExpressionDef;
component: () => ViewDefinition;
template: ViewDefinition;
} }
export enum NodeType { export enum NodeType {
Element, Element,
Text, Text,
Anchor, Provider,
Provider PureExpression
} }
/** /**
@ -109,6 +108,7 @@ export interface ElementDef {
name: string; name: string;
attrs: {[name: string]: string}; attrs: {[name: string]: string};
outputs: ElementOutputDef[]; outputs: ElementOutputDef[];
template: ViewDefinition;
} }
export interface ElementOutputDef { export interface ElementOutputDef {
@ -140,17 +140,31 @@ export interface ProviderDef {
ctor: any; ctor: any;
deps: DepDef[]; deps: DepDef[];
outputs: ProviderOutputDef[]; outputs: ProviderOutputDef[];
// closure to allow recursive components
component: () => ViewDefinition;
} }
export interface TextDef { prefix: string; } export interface TextDef { prefix: string; }
export interface PureExpressionDef {
type: PureExpressionType;
pipeDep: DepDef;
}
export enum PureExpressionType {
Array,
Object,
Pipe
}
export enum BindingType { export enum BindingType {
ElementAttribute, ElementAttribute,
ElementClass, ElementClass,
ElementStyle, ElementStyle,
ElementProperty, ElementProperty,
ProviderProperty, ProviderProperty,
Interpolation Interpolation,
PureExpressionProperty
} }
export interface BindingDef { export interface BindingDef {
@ -193,11 +207,16 @@ export type DisposableFn = () => void;
*/ */
export interface NodeData { export interface NodeData {
renderNode: any; renderNode: any;
provider: any; provider: PureExpressionData|any;
componentView: ViewData; componentView: ViewData;
embeddedViews: ViewData[]; embeddedViews: ViewData[];
} }
export interface PureExpressionData {
value: any;
pipe: PipeTransform;
}
export interface Services { export interface Services {
renderComponent(rcp: RenderComponentType): Renderer; renderComponent(rcp: RenderComponentType): Renderer;
sanitize(context: SecurityContext, value: string): string; sanitize(context: SecurityContext, value: string): string;

View File

@ -9,11 +9,11 @@
import {ExpressionChangedAfterItHasBeenCheckedError} from '../linker/errors'; import {ExpressionChangedAfterItHasBeenCheckedError} from '../linker/errors';
import {RenderComponentType, Renderer} from '../render/api'; import {RenderComponentType, Renderer} from '../render/api';
import {createAnchor} from './anchor';
import {checkAndUpdateElementDynamic, checkAndUpdateElementInline, createElement} from './element'; import {checkAndUpdateElementDynamic, checkAndUpdateElementInline, createElement} from './element';
import {callLifecycleHooksChildrenFirst, checkAndUpdateProviderDynamic, checkAndUpdateProviderInline, createProvider} from './provider'; import {callLifecycleHooksChildrenFirst, checkAndUpdateProviderDynamic, checkAndUpdateProviderInline, createProvider} from './provider';
import {checkAndUpdatePureExpressionDynamic, checkAndUpdatePureExpressionInline, createPureExpression} from './pure_expression';
import {checkAndUpdateTextDynamic, checkAndUpdateTextInline, createText} from './text'; import {checkAndUpdateTextDynamic, checkAndUpdateTextInline, createText} from './text';
import {ElementDef, NodeData, NodeDef, NodeFlags, NodeType, NodeUpdater, ProviderDef, Services, TextDef, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn} from './types'; import {ElementDef, NodeData, NodeDef, NodeFlags, NodeType, NodeUpdater, ProviderDef, PureExpressionData, Services, TextDef, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn} from './types';
import {checkBindingNoChanges} from './util'; import {checkBindingNoChanges} from './util';
const NOOP = (): any => undefined; const NOOP = (): any => undefined;
@ -123,16 +123,17 @@ function calculateReverseChildIndex(
} }
function validateNode(parent: NodeDef, node: NodeDef) { function validateNode(parent: NodeDef, node: NodeDef) {
if (node.template) { const template = node.element && node.element.template;
if (node.template.lastRootNode != null && if (template) {
node.template.nodes[node.template.lastRootNode].flags & NodeFlags.HasEmbeddedViews) { if (template.lastRootNode != null &&
template.nodes[template.lastRootNode].flags & NodeFlags.HasEmbeddedViews) {
throw new Error( throw new Error(
`Illegal State: Last root node of a template can't have embedded views, at index ${node.index}!`); `Illegal State: Last root node of a template can't have embedded views, at index ${node.index}!`);
} }
} }
if (node.provider) { if (node.provider) {
const parentType = parent ? parent.type : null; const parentType = parent ? parent.type : null;
if (parentType !== NodeType.Element && parentType !== NodeType.Anchor) { if (parentType !== NodeType.Element) {
throw new Error( throw new Error(
`Illegal State: Provider nodes need to be children of elements or anchors, at index ${node.index}!`); `Illegal State: Provider nodes need to be children of elements or anchors, at index ${node.index}!`);
} }
@ -175,7 +176,7 @@ function cloneAndModifyNode(nodeDef: NodeDef, values: {
export function createEmbeddedView(parent: ViewData, anchorDef: NodeDef, context?: any): ViewData { export function createEmbeddedView(parent: ViewData, anchorDef: NodeDef, context?: any): ViewData {
// embedded views are seen as siblings to the anchor, so we need // embedded views are seen as siblings to the anchor, so we need
// to get the parent of the anchor and use it as parentIndex. // to get the parent of the anchor and use it as parentIndex.
const view = createView(parent.services, parent, anchorDef.parent, anchorDef.template); const view = createView(parent.services, parent, anchorDef.parent, anchorDef.element.template);
initView(view, null, parent.component, context); initView(view, null, parent.component, context);
return view; return view;
} }
@ -223,16 +224,16 @@ function initView(view: ViewData, renderHost: any, component: any, context: any)
case NodeType.Text: case NodeType.Text:
nodeData = createText(view, renderHost, nodeDef); nodeData = createText(view, renderHost, nodeDef);
break; break;
case NodeType.Anchor:
nodeData = createAnchor(view, renderHost, nodeDef);
break;
case NodeType.Provider: case NodeType.Provider:
let componentView: ViewData; let componentView: ViewData;
if (nodeDef.component) { if (nodeDef.provider.component) {
componentView = createView(view.services, view, i, nodeDef.component()); componentView = createView(view.services, view, i, nodeDef.provider.component());
} }
nodeData = createProvider(view, nodeDef, componentView); nodeData = createProvider(view, nodeDef, componentView);
break; break;
case NodeType.PureExpression:
nodeData = createPureExpression(view, nodeDef);
break;
} }
nodes[i] = nodeData; nodes[i] = nodeData;
} }
@ -272,12 +273,22 @@ const CheckNoChanges: NodeUpdater = {
case 1: case 1:
checkBindingNoChanges(view, nodeDef, 0, v0); checkBindingNoChanges(view, nodeDef, 0, v0);
} }
if (nodeDef.type === NodeType.PureExpression) {
const data: PureExpressionData = view.nodes[index].provider;
return data.value;
}
return undefined;
}, },
checkDynamic: (view: ViewData, index: number, values: any[]): void => { checkDynamic: (view: ViewData, index: number, values: any[]): void => {
const oldValues = view.oldValues; const nodeDef = view.def.nodes[index];
for (let i = 0; i < values.length; i++) { for (let i = 0; i < values.length; i++) {
checkBindingNoChanges(view, view.def.nodes[index], i, values[i]); checkBindingNoChanges(view, nodeDef, i, values[i]);
} }
if (nodeDef.type === NodeType.PureExpression) {
const data: PureExpressionData = view.nodes[index].provider;
return data.value;
}
return undefined;
} }
}; };
@ -301,13 +312,17 @@ const CheckAndUpdate: NodeUpdater = {
switch (nodeDef.type) { switch (nodeDef.type) {
case NodeType.Element: case NodeType.Element:
checkAndUpdateElementInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); checkAndUpdateElementInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9);
break; return undefined;
case NodeType.Text: case NodeType.Text:
checkAndUpdateTextInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); checkAndUpdateTextInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9);
break; return undefined;
case NodeType.Provider: case NodeType.Provider:
checkAndUpdateProviderInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); checkAndUpdateProviderInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9);
break; return undefined;
case NodeType.PureExpression:
checkAndUpdatePureExpressionInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9);
const data: PureExpressionData = view.nodes[index].provider;
return data.value;
} }
}, },
checkDynamic: (view: ViewData, index: number, values: any[]): void => { checkDynamic: (view: ViewData, index: number, values: any[]): void => {
@ -315,13 +330,17 @@ const CheckAndUpdate: NodeUpdater = {
switch (nodeDef.type) { switch (nodeDef.type) {
case NodeType.Element: case NodeType.Element:
checkAndUpdateElementDynamic(view, nodeDef, values); checkAndUpdateElementDynamic(view, nodeDef, values);
break; return undefined;
case NodeType.Text: case NodeType.Text:
checkAndUpdateTextDynamic(view, nodeDef, values); checkAndUpdateTextDynamic(view, nodeDef, values);
break; return undefined;
case NodeType.Provider: case NodeType.Provider:
checkAndUpdateProviderDynamic(view, index, nodeDef, values); checkAndUpdateProviderDynamic(view, nodeDef, values);
break; return undefined;
case NodeType.PureExpression:
checkAndUpdatePureExpressionDynamic(view, nodeDef, values);
const data: PureExpressionData = view.nodes[index].provider;
return data.value;
} }
} }
}; };

View File

@ -7,6 +7,7 @@
*/ */
import {RootRenderer} from '@angular/core'; import {RootRenderer} from '@angular/core';
import {NodeUpdater, ViewData} from '@angular/core/src/view/index';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -34,3 +35,21 @@ export function setupAndCheckRenderer(config: {directDom: boolean}) {
afterEach(() => { expect(rootRenderer.renderComponent).toHaveBeenCalled(); }); afterEach(() => { expect(rootRenderer.renderComponent).toHaveBeenCalled(); });
} }
} }
export enum InlineDynamic {
Inline,
Dynamic
}
export const INLINE_DYNAMIC_VALUES = [InlineDynamic.Inline, InlineDynamic.Dynamic];
export function callUpdater(
updater: NodeUpdater, inlineDynamic: InlineDynamic, view: ViewData, nodeIndex: number,
values: any[]): any {
switch (inlineDynamic) {
case InlineDynamic.Inline:
return (<any>updater.checkInline)(view, nodeIndex, ...values);
case InlineDynamic.Dynamic:
return updater.checkDynamic(view, nodeIndex, values);
}
}

View File

@ -0,0 +1,149 @@
/**
* @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 {PipeTransform, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core';
import {DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, elementDef, providerDef, pureArrayDef, pureObjectDef, purePipeDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing';
import {INLINE_DYNAMIC_VALUES, InlineDynamic, callUpdater} from './helper';
export function main() {
describe(`View Pure Expressions`, () => {
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): {rootNodes: any[], view: ViewData} {
const view = createRootView(services, viewDef);
const rootNodes = rootRenderNodes(view);
return {rootNodes, view};
}
class Service {
data: any;
}
INLINE_DYNAMIC_VALUES.forEach((inlineDynamic) => {
it(`should support pure arrays in ${InlineDynamic[inlineDynamic]} bindings`, () => {
let values: any[];
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, 2, 'span'), pureArrayDef(2),
providerDef(NodeFlags.None, Service, [], {data: [0, 'data']})
],
(updater, view) => {
callUpdater(
updater, inlineDynamic, view, 2,
[callUpdater(updater, inlineDynamic, view, 1, values)]);
}));
const service = view.nodes[2].provider;
values = [1, 2];
checkAndUpdateView(view);
const arr0 = service.data;
expect(arr0).toEqual([1, 2]);
// instance should not change
// if the values don't change
checkAndUpdateView(view);
expect(service.data).toBe(arr0);
values = [3, 2];
checkAndUpdateView(view);
const arr1 = service.data;
expect(arr1).not.toBe(arr0);
expect(arr1).toEqual([3, 2]);
});
});
INLINE_DYNAMIC_VALUES.forEach((inlineDynamic) => {
it(`should support pure objects in ${InlineDynamic[inlineDynamic]} bindings`, () => {
let values: any[];
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, 2, 'span'), pureObjectDef(['a', 'b']),
providerDef(NodeFlags.None, Service, [], {data: [0, 'data']})
],
(updater, view) => {
callUpdater(
updater, inlineDynamic, view, 2,
[callUpdater(updater, inlineDynamic, view, 1, values)]);
}));
const service = view.nodes[2].provider;
values = [1, 2];
checkAndUpdateView(view);
const obj0 = service.data;
expect(obj0).toEqual({a: 1, b: 2});
// instance should not change
// if the values don't change
checkAndUpdateView(view);
expect(service.data).toBe(obj0);
values = [3, 2];
checkAndUpdateView(view);
const obj1 = service.data;
expect(obj1).not.toBe(obj0);
expect(obj1).toEqual({a: 3, b: 2});
});
});
INLINE_DYNAMIC_VALUES.forEach((inlineDynamic) => {
it(`should support pure pipes in ${InlineDynamic[inlineDynamic]} bindings`, () => {
class SomePipe implements PipeTransform {
transform(v1: any, v2: any) { return [v1 + 10, v2 + 20]; }
}
let values: any[];
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, 3, 'span'), providerDef(NodeFlags.None, SomePipe, []),
purePipeDef(SomePipe, 2),
providerDef(NodeFlags.None, Service, [], {data: [0, 'data']})
],
(updater, view) => {
callUpdater(
updater, inlineDynamic, view, 3,
[callUpdater(updater, inlineDynamic, view, 2, values)]);
}));
const service = view.nodes[3].provider;
values = [1, 2];
checkAndUpdateView(view);
const obj0 = service.data;
expect(obj0).toEqual([11, 22]);
// instance should not change
// if the values don't change
checkAndUpdateView(view);
expect(service.data).toBe(obj0);
values = [3, 2];
checkAndUpdateView(view);
const obj1 = service.data;
expect(obj1).not.toBe(obj0);
expect(obj1).toEqual([13, 22]);
});
});
});
}