From 65417374f12df4a7a62d6d676ed1805c2e1c8ae3 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Fri, 20 Jan 2017 09:21:09 -0800 Subject: [PATCH] feat(core): add pure expression support to view engine Part of #14013 --- modules/@angular/core/src/view/anchor.ts | 52 ---- modules/@angular/core/src/view/element.ts | 36 ++- modules/@angular/core/src/view/index.ts | 4 +- modules/@angular/core/src/view/provider.ts | 9 +- .../@angular/core/src/view/pure_expression.ts | 227 ++++++++++++++++++ modules/@angular/core/src/view/text.ts | 13 +- modules/@angular/core/src/view/types.ts | 39 ++- modules/@angular/core/src/view/view.ts | 61 +++-- modules/@angular/core/test/view/helper.ts | 19 ++ .../core/test/view/pure_expression_spec.ts | 149 ++++++++++++ 10 files changed, 508 insertions(+), 101 deletions(-) delete mode 100644 modules/@angular/core/src/view/anchor.ts create mode 100644 modules/@angular/core/src/view/pure_expression.ts create mode 100644 modules/@angular/core/test/view/pure_expression_spec.ts diff --git a/modules/@angular/core/src/view/anchor.ts b/modules/@angular/core/src/view/anchor.ts deleted file mode 100644 index 722dc8b1b8..0000000000 --- a/modules/@angular/core/src/view/anchor.ts +++ /dev/null @@ -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 - }; -} diff --git a/modules/@angular/core/src/view/element.ts b/modules/@angular/core/src/view/element.ts index bf3eb85541..303758f120 100644 --- a/modules/@angular/core/src/view/element.ts +++ b/modules/@angular/core/src/view/element.ts @@ -8,9 +8,33 @@ 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'; +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( flags: NodeFlags, childCount: number, name: string, fixedAttrs: {[name: string]: string} = {}, bindings?: @@ -65,11 +89,10 @@ export function elementDef( childCount, bindings: bindingDefs, disposableCount: outputDefs.length, - element: {name, attrs: fixedAttrs, outputs: outputDefs}, + element: {name, attrs: fixedAttrs, outputs: outputDefs, template: undefined}, provider: undefined, text: undefined, - component: undefined, - template: undefined + pureExpression: undefined }; } @@ -78,9 +101,10 @@ export function createElement(view: ViewData, renderHost: any, def: NodeDef): No const elDef = def.element; let el: any; if (view.renderer) { - el = view.renderer.createElement(parentNode, elDef.name); + el = elDef.name ? view.renderer.createElement(parentNode, elDef.name) : + view.renderer.createTemplateAnchor(parentNode); } else { - el = document.createElement(elDef.name); + el = elDef.name ? document.createElement(elDef.name) : document.createComment(''); if (parentNode) { parentNode.appendChild(el); } diff --git a/modules/@angular/core/src/view/index.ts b/modules/@angular/core/src/view/index.ts index 42ee2c0544..a530aeb2f4 100644 --- a/modules/@angular/core/src/view/index.ts +++ b/modules/@angular/core/src/view/index.ts @@ -6,9 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -export {anchorDef} from './anchor'; -export {elementDef} from './element'; +export {anchorDef, elementDef} from './element'; export {providerDef} from './provider'; +export {pureArrayDef, pureObjectDef, purePipeDef} from './pure_expression'; export {textDef} from './text'; export {checkAndUpdateView, checkNoChangesView, createEmbeddedView, createRootView, destroyView, viewDef} from './view'; export {attachEmbeddedView, detachEmbeddedView, rootRenderNodes} from './view_attach'; diff --git a/modules/@angular/core/src/view/provider.ts b/modules/@angular/core/src/view/provider.ts index 8762da500e..acfa222943 100644 --- a/modules/@angular/core/src/view/provider.ts +++ b/modules/@angular/core/src/view/provider.ts @@ -75,9 +75,9 @@ export function providerDef( childCount: 0, bindings, disposableCount: outputDefs.length, element: undefined, - provider: {tokenKey: tokenKey(ctor), ctor, deps: depDefs, outputs: outputDefs}, - text: undefined, component, - template: undefined + provider: {tokenKey: tokenKey(ctor), ctor, deps: depDefs, outputs: outputDefs, component}, + text: undefined, + pureExpression: undefined }; } @@ -147,8 +147,7 @@ export function checkAndUpdateProviderInline( } } -export function checkAndUpdateProviderDynamic( - view: ViewData, index: number, def: NodeDef, values: any[]) { +export function checkAndUpdateProviderDynamic(view: ViewData, def: NodeDef, values: any[]) { const provider = view.nodes[def.index].provider; let changes: SimpleChanges; for (let i = 0; i < values.length; i++) { diff --git a/modules/@angular/core/src/view/pure_expression.ts b/modules/@angular/core/src/view/pure_expression.ts new file mode 100644 index 0000000000..baa40449ac --- /dev/null +++ b/modules/@angular/core/src/view/pure_expression.ts @@ -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; + } +} diff --git a/modules/@angular/core/src/view/text.ts b/modules/@angular/core/src/view/text.ts index af2b84cc9b..7b093fe2ab 100644 --- a/modules/@angular/core/src/view/text.ts +++ b/modules/@angular/core/src/view/text.ts @@ -39,8 +39,7 @@ export function textDef(constants: string[]): NodeDef { element: undefined, provider: undefined, text: {prefix: constants[0]}, - component: undefined, - template: undefined + pureExpression: undefined }; } @@ -124,9 +123,13 @@ export function checkAndUpdateTextInline( export function checkAndUpdateTextDynamic(view: ViewData, def: NodeDef, values: any[]) { const bindings = def.bindings; - let changed = view.firstChange; - for (let i = 0; i < values.length && !changed; i++) { - changed = changed || checkAndUpdateBinding(view, def, i, values[i]); + 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) { let value = ''; diff --git a/modules/@angular/core/src/view/types.ts b/modules/@angular/core/src/view/types.ts index 3f37c5c4e4..3b1682536b 100644 --- a/modules/@angular/core/src/view/types.ts +++ b/modules/@angular/core/src/view/types.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {PipeTransform} from '../change_detection/change_detection'; import {TemplateRef} from '../linker/template_ref'; import {ViewContainerRef} from '../linker/view_container_ref'; import {RenderComponentType, Renderer, RootRenderer} from '../render/api'; @@ -42,8 +43,8 @@ export type ViewUpdateFn = (updater: NodeUpdater, view: ViewData) => void; 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): void; - checkDynamic(view: ViewData, nodeIndex: number, values: any[]): void; + v6?: any, v7?: any, v8?: any, v9?: any): any; + checkDynamic(view: ViewData, nodeIndex: number, values: any[]): any; } export type ViewHandleEventFn = @@ -68,24 +69,22 @@ export interface NodeDef { childCount: number; /** aggregated NodeFlags for all children **/ childFlags: NodeFlags; + providerIndices: {[tokenKey: string]: number}; bindingIndex: number; bindings: BindingDef[]; disposableIndex: number; disposableCount: number; element: ElementDef; - providerIndices: {[tokenKey: string]: number}; provider: ProviderDef; text: TextDef; - // closure to allow recursive components - component: () => ViewDefinition; - template: ViewDefinition; + pureExpression: PureExpressionDef; } export enum NodeType { Element, Text, - Anchor, - Provider + Provider, + PureExpression } /** @@ -109,6 +108,7 @@ export interface ElementDef { name: string; attrs: {[name: string]: string}; outputs: ElementOutputDef[]; + template: ViewDefinition; } export interface ElementOutputDef { @@ -140,17 +140,31 @@ export interface ProviderDef { ctor: any; deps: DepDef[]; outputs: ProviderOutputDef[]; + // closure to allow recursive components + component: () => ViewDefinition; } export interface TextDef { prefix: string; } +export interface PureExpressionDef { + type: PureExpressionType; + pipeDep: DepDef; +} + +export enum PureExpressionType { + Array, + Object, + Pipe +} + export enum BindingType { ElementAttribute, ElementClass, ElementStyle, ElementProperty, ProviderProperty, - Interpolation + Interpolation, + PureExpressionProperty } export interface BindingDef { @@ -193,11 +207,16 @@ export type DisposableFn = () => void; */ export interface NodeData { renderNode: any; - provider: any; + provider: PureExpressionData|any; componentView: ViewData; embeddedViews: ViewData[]; } +export interface PureExpressionData { + value: any; + pipe: PipeTransform; +} + export interface Services { renderComponent(rcp: RenderComponentType): Renderer; sanitize(context: SecurityContext, value: string): string; diff --git a/modules/@angular/core/src/view/view.ts b/modules/@angular/core/src/view/view.ts index c7c4aae708..a3c5bfae67 100644 --- a/modules/@angular/core/src/view/view.ts +++ b/modules/@angular/core/src/view/view.ts @@ -9,11 +9,11 @@ import {ExpressionChangedAfterItHasBeenCheckedError} from '../linker/errors'; import {RenderComponentType, Renderer} from '../render/api'; -import {createAnchor} from './anchor'; import {checkAndUpdateElementDynamic, checkAndUpdateElementInline, createElement} from './element'; import {callLifecycleHooksChildrenFirst, checkAndUpdateProviderDynamic, checkAndUpdateProviderInline, createProvider} from './provider'; +import {checkAndUpdatePureExpressionDynamic, checkAndUpdatePureExpressionInline, createPureExpression} from './pure_expression'; 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'; const NOOP = (): any => undefined; @@ -123,16 +123,17 @@ function calculateReverseChildIndex( } function validateNode(parent: NodeDef, node: NodeDef) { - if (node.template) { - if (node.template.lastRootNode != null && - node.template.nodes[node.template.lastRootNode].flags & NodeFlags.HasEmbeddedViews) { + const template = node.element && node.element.template; + if (template) { + if (template.lastRootNode != null && + template.nodes[template.lastRootNode].flags & NodeFlags.HasEmbeddedViews) { throw new Error( `Illegal State: Last root node of a template can't have embedded views, at index ${node.index}!`); } } if (node.provider) { const parentType = parent ? parent.type : null; - if (parentType !== NodeType.Element && parentType !== NodeType.Anchor) { + if (parentType !== NodeType.Element) { throw new Error( `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 { // embedded views are seen as siblings to the anchor, so we need // 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); return view; } @@ -223,16 +224,16 @@ function initView(view: ViewData, renderHost: any, component: any, context: any) case NodeType.Text: nodeData = createText(view, renderHost, nodeDef); break; - case NodeType.Anchor: - nodeData = createAnchor(view, renderHost, nodeDef); - break; case NodeType.Provider: let componentView: ViewData; - if (nodeDef.component) { - componentView = createView(view.services, view, i, nodeDef.component()); + if (nodeDef.provider.component) { + componentView = createView(view.services, view, i, nodeDef.provider.component()); } nodeData = createProvider(view, nodeDef, componentView); break; + case NodeType.PureExpression: + nodeData = createPureExpression(view, nodeDef); + break; } nodes[i] = nodeData; } @@ -272,12 +273,22 @@ const CheckNoChanges: NodeUpdater = { case 1: 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 => { - const oldValues = view.oldValues; + const nodeDef = view.def.nodes[index]; 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) { case NodeType.Element: checkAndUpdateElementInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); - break; + return undefined; case NodeType.Text: checkAndUpdateTextInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); - break; + return undefined; case NodeType.Provider: 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 => { @@ -315,13 +330,17 @@ const CheckAndUpdate: NodeUpdater = { switch (nodeDef.type) { case NodeType.Element: checkAndUpdateElementDynamic(view, nodeDef, values); - break; + return undefined; case NodeType.Text: checkAndUpdateTextDynamic(view, nodeDef, values); - break; + return undefined; case NodeType.Provider: - checkAndUpdateProviderDynamic(view, index, nodeDef, values); - break; + checkAndUpdateProviderDynamic(view, nodeDef, values); + return undefined; + case NodeType.PureExpression: + checkAndUpdatePureExpressionDynamic(view, nodeDef, values); + const data: PureExpressionData = view.nodes[index].provider; + return data.value; } } }; diff --git a/modules/@angular/core/test/view/helper.ts b/modules/@angular/core/test/view/helper.ts index 1a77d32205..ec5dd1f741 100644 --- a/modules/@angular/core/test/view/helper.ts +++ b/modules/@angular/core/test/view/helper.ts @@ -7,6 +7,7 @@ */ import {RootRenderer} from '@angular/core'; +import {NodeUpdater, ViewData} from '@angular/core/src/view/index'; import {TestBed} from '@angular/core/testing'; 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(); }); } } + +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 (updater.checkInline)(view, nodeIndex, ...values); + case InlineDynamic.Dynamic: + return updater.checkDynamic(view, nodeIndex, values); + } +} diff --git a/modules/@angular/core/test/view/pure_expression_spec.ts b/modules/@angular/core/test/view/pure_expression_spec.ts new file mode 100644 index 0000000000..17ef219e72 --- /dev/null +++ b/modules/@angular/core/test/view/pure_expression_spec.ts @@ -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]); + }); + }); + }); +}