From 1e729d7ba20cee417259934da9424b50e2d41548 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Mon, 23 Jan 2017 16:59:20 -0800 Subject: [PATCH] feat(core): add query support to view engine Part of #14013 closes #14084 --- modules/@angular/core/src/view/element.ts | 29 +- modules/@angular/core/src/view/provider.ts | 215 +++++++++- .../@angular/core/src/view/pure_expression.ts | 2 + modules/@angular/core/src/view/text.ts | 4 +- modules/@angular/core/src/view/types.ts | 50 ++- modules/@angular/core/src/view/util.ts | 8 + modules/@angular/core/src/view/view.ts | 51 ++- modules/@angular/core/src/view/view_attach.ts | 76 +++- .../@angular/core/test/view/anchor_spec.ts | 8 +- .../core/test/view/component_view_spec.ts | 24 +- .../@angular/core/test/view/element_spec.ts | 38 +- .../core/test/view/embedded_view_spec.ts | 37 +- .../@angular/core/test/view/provider_spec.ts | 75 ++-- .../core/test/view/pure_expression_spec.ts | 14 +- modules/@angular/core/test/view/query_spec.ts | 390 ++++++++++++++++++ modules/@angular/core/test/view/text_spec.ts | 2 +- .../@angular/core/test/view/view_def_spec.ts | 158 +++++-- modules/benchmarks/src/tree/ng2_next/tree.ts | 25 +- 18 files changed, 1022 insertions(+), 184 deletions(-) create mode 100644 modules/@angular/core/test/view/query_spec.ts diff --git a/modules/@angular/core/src/view/element.ts b/modules/@angular/core/src/view/element.ts index 0d59bb4a40..498dfbe684 100644 --- a/modules/@angular/core/src/view/element.ts +++ b/modules/@angular/core/src/view/element.ts @@ -8,11 +8,16 @@ import {SecurityContext} from '../security'; -import {BindingDef, BindingType, DisposableFn, ElementOutputDef, NodeData, NodeDef, NodeFlags, NodeType, ViewData, ViewDefinition, ViewFlags} from './types'; +import {BindingDef, BindingType, DisposableFn, ElementOutputDef, NodeData, NodeDef, NodeFlags, NodeType, QueryValueType, ViewData, ViewDefinition, ViewFlags} from './types'; import {checkAndUpdateBinding, setBindingDebugInfo} from './util'; export function anchorDef( - flags: NodeFlags, childCount: number, template?: ViewDefinition): NodeDef { + flags: NodeFlags, matchedQueries: [string, QueryValueType][], childCount: number, + template?: ViewDefinition): NodeDef { + const matchedQueryDefs: {[queryId: string]: QueryValueType} = {}; + if (matchedQueries) { + matchedQueries.forEach(([queryId, valueType]) => { matchedQueryDefs[queryId] = valueType; }); + } return { type: NodeType.Element, // will bet set by the view definition @@ -20,12 +25,13 @@ export function anchorDef( reverseChildIndex: undefined, parent: undefined, childFlags: undefined, + childMatchedQueries: undefined, bindingIndex: undefined, disposableIndex: undefined, providerIndices: undefined, // regular values flags, - childCount, + matchedQueries: matchedQueryDefs, childCount, bindings: [], disposableCount: 0, element: {name: undefined, attrs: undefined, outputs: [], template}, @@ -36,11 +42,16 @@ export function anchorDef( } export function elementDef( - flags: NodeFlags, childCount: number, name: string, fixedAttrs: {[name: string]: string} = {}, + flags: NodeFlags, matchedQueries: [string, QueryValueType][], childCount: number, name: string, + fixedAttrs: {[name: string]: string} = {}, bindings?: ([BindingType.ElementClass, string] | [BindingType.ElementStyle, string, string] | [BindingType.ElementAttribute | BindingType.ElementProperty, string, SecurityContext])[], outputs?: (string | [string, string])[]): NodeDef { + const matchedQueryDefs: {[queryId: string]: QueryValueType} = {}; + if (matchedQueries) { + matchedQueries.forEach(([queryId, valueType]) => { matchedQueryDefs[queryId] = valueType; }); + } bindings = bindings || []; const bindingDefs = new Array(bindings.length); for (let i = 0; i < bindings.length; i++) { @@ -81,12 +92,13 @@ export function elementDef( reverseChildIndex: undefined, parent: undefined, childFlags: undefined, + childMatchedQueries: undefined, bindingIndex: undefined, disposableIndex: undefined, providerIndices: undefined, // regular values flags, - childCount, + matchedQueries: matchedQueryDefs, childCount, bindings: bindingDefs, disposableCount: outputDefs.length, element: {name, attrs: fixedAttrs, outputs: outputDefs, template: undefined}, @@ -150,8 +162,11 @@ export function createElement(view: ViewData, renderHost: any, def: NodeDef): No } } return { - elementOrText: - {node: el, embeddedViews: (def.flags & NodeFlags.HasEmbeddedViews) ? [] : undefined}, + elementOrText: { + node: el, + embeddedViews: (def.flags & NodeFlags.HasEmbeddedViews) ? [] : undefined, + projectedViews: undefined + }, provider: undefined, pureExpression: undefined, }; diff --git a/modules/@angular/core/src/view/provider.ts b/modules/@angular/core/src/view/provider.ts index b0f1a4c6e0..b005aaadfc 100644 --- a/modules/@angular/core/src/view/provider.ts +++ b/modules/@angular/core/src/view/provider.ts @@ -10,12 +10,14 @@ import {SimpleChange, SimpleChanges} from '../change_detection/change_detection' import {Injector} from '../di'; import {stringify} from '../facade/lang'; 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'; import {Renderer} from '../render/api'; -import {BindingDef, BindingType, DepDef, DepFlags, DisposableFn, NodeData, NodeDef, NodeFlags, NodeType, ProviderOutputDef, Services, ViewData, ViewDefinition, ViewFlags} from './types'; -import {checkAndUpdateBinding, checkAndUpdateBindingWithChange, setBindingDebugInfo} from './util'; +import {BindingDef, BindingType, DepDef, DepFlags, DisposableFn, NodeData, NodeDef, NodeFlags, NodeType, ProviderOutputDef, QueryBindingType, QueryDef, QueryValueType, Services, ViewData, ViewDefinition, ViewFlags} from './types'; +import {checkAndUpdateBinding, checkAndUpdateBindingWithChange, declaredViewContainer, setBindingDebugInfo} from './util'; const _tokenKeyCache = new Map(); @@ -25,9 +27,16 @@ const ViewContainerRefTokenKey = tokenKey(ViewContainerRef); const TemplateRefTokenKey = tokenKey(TemplateRef); export function providerDef( - flags: NodeFlags, ctor: any, deps: ([DepFlags, any] | any)[], - props?: {[name: string]: [number, string]}, outputs?: {[name: string]: string}, - component?: () => ViewDefinition): NodeDef { + flags: NodeFlags, matchedQueries: [string, QueryValueType][], ctor: any, + deps: ([DepFlags, any] | any)[], props?: {[name: string]: [number, string]}, + outputs?: {[name: string]: string}, + contentQueries?: {[name: string]: [string, QueryBindingType]}, component?: () => ViewDefinition, + viewQueries?: {[name: string]: [string, QueryBindingType]}, ): NodeDef { + const matchedQueryDefs: {[queryId: string]: QueryValueType} = {}; + if (matchedQueries) { + matchedQueries.forEach(([queryId, valueType]) => { matchedQueryDefs[queryId] = valueType; }); + } + const bindings: BindingDef[] = []; if (props) { for (let prop in props) { @@ -57,9 +66,27 @@ export function providerDef( } return {flags, token, tokenKey: tokenKey(token)}; }); + const contentQueryDefs: QueryDef[] = []; + for (let propName in contentQueries) { + const [id, bindingType] = contentQueries[propName]; + contentQueryDefs.push({id, propName, bindingType}); + } + const viewQueryDefs: QueryDef[] = []; + for (let propName in viewQueries) { + const [id, bindingType] = viewQueries[propName]; + viewQueryDefs.push({id, propName, bindingType}); + } + if (component) { flags = flags | NodeFlags.HasComponent; } + if (contentQueryDefs.length) { + flags = flags | NodeFlags.HasContentQuery; + } + if (viewQueryDefs.length) { + flags = flags | NodeFlags.HasViewQuery; + } + return { type: NodeType.Provider, // will bet set by the view definition @@ -67,17 +94,26 @@ export function providerDef( reverseChildIndex: undefined, parent: undefined, childFlags: undefined, + childMatchedQueries: undefined, bindingIndex: undefined, disposableIndex: undefined, providerIndices: undefined, // regular values flags, + matchedQueries: matchedQueryDefs, childCount: 0, bindings, disposableCount: outputDefs.length, element: undefined, - provider: {tokenKey: tokenKey(ctor), ctor, deps: depDefs, outputs: outputDefs, component}, + provider: { + tokenKey: tokenKey(ctor), + ctor, + deps: depDefs, + outputs: outputDefs, + contentQueries: contentQueryDefs, + viewQueries: viewQueryDefs, component + }, text: undefined, - pureExpression: undefined + pureExpression: undefined, }; } @@ -101,9 +137,21 @@ export function createProvider(view: ViewData, def: NodeDef, componentView: View view.disposables[def.disposableIndex + i] = subscription.unsubscribe.bind(subscription); } } + let queries: {[queryId: string]: QueryList}; + if (providerDef.contentQueries.length || providerDef.viewQueries.length) { + queries = {}; + for (let i = 0; i < providerDef.contentQueries.length; i++) { + const def = providerDef.contentQueries[i]; + queries[def.id] = new QueryList(); + } + for (let i = 0; i < providerDef.viewQueries.length; i++) { + const def = providerDef.viewQueries[i]; + queries[def.id] = new QueryList(); + } + } return { elementOrText: undefined, - provider: {instance: provider, componentView: componentView}, + provider: {instance: provider, componentView: componentView, queries}, pureExpression: undefined, }; } @@ -202,7 +250,7 @@ export function resolveDep( if (elDef.parent != null) { elIndex = elDef.parent; } else { - elIndex = view.parentIndex; + elIndex = view.parentDiIndex; view = view.parent; } } @@ -228,7 +276,7 @@ export function resolveDep( return view.nodes[providerIndex].provider.instance; } } - elIndex = view.parentIndex; + elIndex = view.parentDiIndex; view = view.parent; } return Injector.NULL.get(depDef.token, notFoundValue); @@ -265,6 +313,153 @@ function checkAndUpdateProp( return changes; } +export enum QueryAction { + CheckNoChanges, + CheckAndUpdate, +} + +export function execContentQueriesAction(view: ViewData, action: QueryAction) { + if (!(view.def.nodeFlags & NodeFlags.HasContentQuery)) { + return; + } + for (let i = 0; i < view.nodes.length; i++) { + const nodeDef = view.def.nodes[i]; + if (nodeDef.flags & NodeFlags.HasContentQuery) { + execContentQuery(view, nodeDef, action); + } else if ((nodeDef.childFlags & NodeFlags.HasContentQuery) === 0) { + // no child has a content query + // then skip the children + i += nodeDef.childCount; + } + } +} + +export function updateViewQueries(view: ViewData, action: QueryAction) { + if (!(view.def.nodeFlags & NodeFlags.HasViewQuery)) { + return; + } + for (let i = 0; i < view.nodes.length; i++) { + const nodeDef = view.def.nodes[i]; + if (nodeDef.flags & NodeFlags.HasViewQuery) { + updateViewQuery(view, nodeDef, action); + } else if ((nodeDef.childFlags & NodeFlags.HasViewQuery) === 0) { + // no child has a view query + // then skip the children + i += nodeDef.childCount; + } + } +} + +function execContentQuery(view: ViewData, nodeDef: NodeDef, action: QueryAction) { + const providerData = view.nodes[nodeDef.index].provider; + for (let i = 0; i < nodeDef.provider.contentQueries.length; i++) { + const queryDef = nodeDef.provider.contentQueries[i]; + const queryId = queryDef.id; + const queryList = providerData.queries[queryId]; + if (queryList.dirty) { + const elementDef = view.def.nodes[nodeDef.parent]; + const newValues = calcQueryValues( + view, elementDef.index, elementDef.index + elementDef.childCount, queryId, []); + execQueryAction(view, providerData.instance, queryList, queryDef, newValues, action); + } + } +} + +function updateViewQuery(view: ViewData, nodeDef: NodeDef, action: QueryAction) { + for (let i = 0; i < nodeDef.provider.viewQueries.length; i++) { + const queryDef = nodeDef.provider.viewQueries[i]; + const queryId = queryDef.id; + const providerData = view.nodes[nodeDef.index].provider; + const queryList = providerData.queries[queryId]; + if (queryList.dirty) { + const componentView = providerData.componentView; + const newValues = + calcQueryValues(componentView, 0, componentView.nodes.length - 1, queryId, []); + execQueryAction(view, providerData.instance, queryList, queryDef, newValues, action); + } + } +} + +function execQueryAction( + view: ViewData, provider: any, queryList: QueryList, queryDef: QueryDef, newValues: any[], + action: QueryAction) { + switch (action) { + case QueryAction.CheckAndUpdate: + queryList.reset(newValues); + let boundValue: any; + switch (queryDef.bindingType) { + case QueryBindingType.First: + boundValue = queryList.first; + break; + case QueryBindingType.All: + boundValue = queryList; + break; + } + provider[queryDef.propName] = boundValue; + break; + case QueryAction.CheckNoChanges: + // queries should always be non dirty when we go into checkNoChanges! + const oldValuesStr = queryList.toArray().map(v => stringify(v)); + const newValuesStr = newValues.map(v => stringify(v)); + throw new ExpressionChangedAfterItHasBeenCheckedError( + oldValuesStr, newValuesStr, view.firstChange); + } +} + +function calcQueryValues( + view: ViewData, startIndex: number, endIndex: number, queryId: string, values: any[]): any[] { + const len = view.def.nodes.length; + for (let i = startIndex; i <= endIndex; i++) { + const nodeDef = view.def.nodes[i]; + const queryValueType = nodeDef.matchedQueries[queryId]; + if (queryValueType != null) { + // a match + let value: any; + switch (queryValueType) { + case QueryValueType.ElementRef: + value = new ElementRef(view.nodes[i].elementOrText.node); + break; + case QueryValueType.TemplateRef: + value = view.services.createTemplateRef(view, nodeDef); + break; + case QueryValueType.ViewContainerRef: + value = view.services.createViewContainerRef(view.nodes[i]); + break; + case QueryValueType.Provider: + value = view.nodes[i].provider.instance; + break; + } + values.push(value); + } + if (nodeDef.flags & NodeFlags.HasEmbeddedViews && + queryId in nodeDef.element.template.nodeMatchedQueries) { + // check embedded views that were attached at the place of their template. + const nodeData = view.nodes[i]; + const embeddedViews = nodeData.elementOrText.embeddedViews; + for (let k = 0; k < embeddedViews.length; k++) { + const embeddedView = embeddedViews[k]; + const dvc = declaredViewContainer(embeddedView); + if (dvc && dvc === nodeData) { + calcQueryValues(embeddedView, 0, embeddedView.nodes.length - 1, queryId, values); + } + } + const projectedViews = nodeData.elementOrText.projectedViews; + if (projectedViews) { + for (let k = 0; k < projectedViews.length; k++) { + const projectedView = projectedViews[k]; + calcQueryValues(projectedView, 0, projectedView.nodes.length - 1, queryId, values); + } + } + } + if (!(queryId in nodeDef.childMatchedQueries)) { + // If don't check descendants, skip the children. + // Or: no child matches the query, then skip the children as well. + i += nodeDef.childCount; + } + } + return values; +} + export function callLifecycleHooksChildrenFirst(view: ViewData, lifecycles: NodeFlags) { if (!(view.def.nodeFlags & lifecycles)) { return; diff --git a/modules/@angular/core/src/view/pure_expression.ts b/modules/@angular/core/src/view/pure_expression.ts index 2e5363bba8..818b7343b9 100644 --- a/modules/@angular/core/src/view/pure_expression.ts +++ b/modules/@angular/core/src/view/pure_expression.ts @@ -44,11 +44,13 @@ function _pureExpressionDef( reverseChildIndex: undefined, parent: undefined, childFlags: undefined, + childMatchedQueries: undefined, bindingIndex: undefined, disposableIndex: undefined, providerIndices: undefined, // regular values flags: 0, + matchedQueries: {}, childCount: 0, bindings, disposableCount: 0, element: undefined, diff --git a/modules/@angular/core/src/view/text.ts b/modules/@angular/core/src/view/text.ts index 7ac3f25aba..457a87f71b 100644 --- a/modules/@angular/core/src/view/text.ts +++ b/modules/@angular/core/src/view/text.ts @@ -29,11 +29,13 @@ export function textDef(constants: string[]): NodeDef { reverseChildIndex: undefined, parent: undefined, childFlags: undefined, + childMatchedQueries: undefined, bindingIndex: undefined, disposableIndex: undefined, providerIndices: undefined, // regular values flags: 0, + matchedQueries: {}, childCount: 0, bindings, disposableCount: 0, element: undefined, @@ -55,7 +57,7 @@ export function createText(view: ViewData, renderHost: any, def: NodeDef): NodeD } } return { - elementOrText: {node: renderNode, embeddedViews: undefined}, + elementOrText: {node: renderNode, embeddedViews: undefined, projectedViews: undefined}, provider: undefined, pureExpression: undefined }; diff --git a/modules/@angular/core/src/view/types.ts b/modules/@angular/core/src/view/types.ts index 8f32c27c36..3e00c9c15d 100644 --- a/modules/@angular/core/src/view/types.ts +++ b/modules/@angular/core/src/view/types.ts @@ -7,6 +7,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'; @@ -36,6 +37,11 @@ export interface ViewDefinition { lastRootNode: number; bindingCount: number; disposableCount: number; + /** + * ids of all queries that are matched by one of the nodes. + * This includes query ids from templates as well. + */ + nodeMatchedQueries: {[queryId: string]: boolean}; } export type ViewUpdateFn = (updater: NodeUpdater, view: ViewData) => void; @@ -69,11 +75,21 @@ export interface NodeDef { childCount: number; /** aggregated NodeFlags for all children **/ childFlags: NodeFlags; + providerIndices: {[tokenKey: string]: number}; bindingIndex: number; bindings: BindingDef[]; disposableIndex: number; disposableCount: number; + /** + * ids and value types of all queries that are matched by this node. + */ + matchedQueries: {[queryId: string]: QueryValueType}; + /** + * ids of all queries that are matched by one of the child nodes. + * This includes query ids from templates as well. + */ + childMatchedQueries: {[queryId: string]: boolean}; element: ElementDef; provider: ProviderDef; text: TextDef; @@ -84,7 +100,7 @@ export enum NodeType { Element, Text, Provider, - PureExpression + PureExpression, } /** @@ -102,6 +118,8 @@ export enum NodeFlags { AfterViewChecked = 1 << 7, HasEmbeddedViews = 1 << 8, HasComponent = 1 << 9, + HasContentQuery = 1 << 10, + HasViewQuery = 1 << 11, } export interface ElementDef { @@ -140,10 +158,30 @@ export interface ProviderDef { ctor: any; deps: DepDef[]; outputs: ProviderOutputDef[]; + contentQueries: QueryDef[]; + viewQueries: QueryDef[]; // closure to allow recursive components component: () => ViewDefinition; } +export interface QueryDef { + id: string; + propName: string; + bindingType: QueryBindingType; +} + +export enum QueryBindingType { + First, + All +} + +export enum QueryValueType { + ElementRef, + TemplateRef, + ViewContainerRef, + Provider +} + export interface TextDef { prefix: string; } export interface PureExpressionDef { @@ -190,6 +228,10 @@ export interface ViewData { // index of parent element / anchor. Not the index // of the provider with the component view. parentIndex: number; + // for component views, this is the same as parentIndex. + // for embedded views, this is the index of the parent node + // that contains the view container. + parentDiIndex: number; parent: ViewData; component: any; context: any; @@ -214,11 +256,17 @@ export interface NodeData { export interface ElementOrTextData { node: any; embeddedViews: ViewData[]; + // views that have been created from the template + // of this element, + // but inserted into the embeddedViews of another element. + // By default, this is undefined. + projectedViews: ViewData[]; } export interface ProviderData { instance: any; componentView: ViewData; + queries: {[queryId: string]: QueryList}; } export interface PureExpressionData { diff --git a/modules/@angular/core/src/view/util.ts b/modules/@angular/core/src/view/util.ts index ebc3b33c20..56074c3fb5 100644 --- a/modules/@angular/core/src/view/util.ts +++ b/modules/@angular/core/src/view/util.ts @@ -60,3 +60,11 @@ export function checkAndUpdateBindingWithChange( } return null; } + +export function declaredViewContainer(view: ViewData) { + if (view.parent) { + const parentView = view.parent; + return parentView.nodes[view.parentIndex]; + } + return undefined; +} diff --git a/modules/@angular/core/src/view/view.ts b/modules/@angular/core/src/view/view.ts index 6999eb9d71..d83f270184 100644 --- a/modules/@angular/core/src/view/view.ts +++ b/modules/@angular/core/src/view/view.ts @@ -10,7 +10,7 @@ import {ExpressionChangedAfterItHasBeenCheckedError} from '../linker/errors'; import {RenderComponentType, Renderer} from '../render/api'; import {checkAndUpdateElementDynamic, checkAndUpdateElementInline, createElement} from './element'; -import {callLifecycleHooksChildrenFirst, checkAndUpdateProviderDynamic, checkAndUpdateProviderInline, createProvider} from './provider'; +import {QueryAction, callLifecycleHooksChildrenFirst, checkAndUpdateProviderDynamic, checkAndUpdateProviderInline, createProvider, execContentQueriesAction, updateViewQueries} from './provider'; import {checkAndUpdatePureExpressionDynamic, checkAndUpdatePureExpressionInline, createPureExpression} from './pure_expression'; import {checkAndUpdateTextDynamic, checkAndUpdateTextInline, createText} from './text'; import {ElementDef, NodeData, NodeDef, NodeFlags, NodeType, NodeUpdater, ProviderData, ProviderDef, Services, TextDef, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn} from './types'; @@ -30,6 +30,7 @@ export function viewDef( let viewBindingCount = 0; let viewDisposableCount = 0; let viewFlags = 0; + let viewMatchedQueries: {[queryId: string]: boolean} = {}; let currentParent: NodeDef = null; let lastRootNode: NodeDef = null; for (let i = 0; i < nodesWithoutIndices.length; i++) { @@ -37,6 +38,7 @@ export function viewDef( const newParent = nodes[currentParent.parent]; if (newParent) { newParent.childFlags |= currentParent.childFlags; + copyInto(currentParent.childMatchedQueries, newParent.childMatchedQueries); } currentParent = newParent; } @@ -54,10 +56,15 @@ export function viewDef( validateNode(currentParent, node); viewFlags |= node.flags; + copyInto(node.matchedQueries, viewMatchedQueries); viewBindingCount += node.bindings.length; viewDisposableCount += node.disposableCount; if (currentParent) { currentParent.childFlags |= node.flags; + copyInto(node.matchedQueries, currentParent.childMatchedQueries); + if (node.element && node.element.template) { + copyInto(node.element.template.nodeMatchedQueries, currentParent.childMatchedQueries); + } } if (!currentParent) { @@ -65,15 +72,29 @@ export function viewDef( } if (node.provider) { currentParent.providerIndices[node.provider.tokenKey] = i; + for (let k = 0; k < node.provider.contentQueries.length; k++) { + currentParent.providerIndices[node.provider.contentQueries[k].id] = i; + } + for (let k = 0; k < node.provider.viewQueries.length; k++) { + currentParent.providerIndices[node.provider.viewQueries[k].id] = i; + } } if (node.childCount) { currentParent = node; } } + while (currentParent) { + const newParent = nodes[currentParent.parent]; + if (newParent) { + newParent.childFlags |= currentParent.childFlags; + copyInto(currentParent.childMatchedQueries, newParent.childMatchedQueries); + } + currentParent = newParent; + } return { nodeFlags: viewFlags, - flags, + nodeMatchedQueries: viewMatchedQueries, flags, nodes: nodes, reverseChildNodes, update: update || NOOP, handleEvent: handleEvent || NOOP, componentType, @@ -83,6 +104,12 @@ export function viewDef( }; } +function copyInto(source: any, target: any) { + for (let prop in source) { + target[prop] = source[prop]; + } +} + function calculateReverseChildIndex( currentParent: NodeDef, i: number, childCount: number, nodeCount: number) { // Notes about reverse child order: @@ -158,9 +185,7 @@ function cloneAndModifyNode(nodeDef: NodeDef, values: { providerIndices: {[tokenKey: string]: number} }): NodeDef { const clonedNode: NodeDef = {}; - for (let prop in nodeDef) { - (clonedNode)[prop] = (nodeDef)[prop]; - } + copyInto(nodeDef, clonedNode); clonedNode.index = values.index; clonedNode.bindingIndex = values.bindingIndex; @@ -170,25 +195,28 @@ function cloneAndModifyNode(nodeDef: NodeDef, values: { clonedNode.providerIndices = values.providerIndices; // Note: We can't set the value immediately, as we need to walk the children first. clonedNode.childFlags = 0; + clonedNode.childMatchedQueries = {}; return clonedNode; } 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.element.template); + const view = createView( + parent.services, parent, anchorDef.index, anchorDef.parent, anchorDef.element.template); initView(view, null, parent.component, context); return view; } export function createRootView(services: Services, def: ViewDefinition, context?: any): ViewData { - const view = createView(services, null, null, def); + const view = createView(services, null, null, null, def); initView(view, null, context, context); return view; } function createView( - services: Services, parent: ViewData, parentIndex: number, def: ViewDefinition): ViewData { + services: Services, parent: ViewData, parentIndex: number, parentDiIndex: number, + def: ViewDefinition): ViewData { const nodes: NodeData[] = new Array(def.nodes.length); let renderer: Renderer; if (def.flags != null && (def.flags & ViewFlags.DirectDom)) { @@ -201,6 +229,7 @@ function createView( def, parent, parentIndex, + parentDiIndex, context: undefined, component: undefined, nodes, firstChange: true, renderer, services, @@ -227,7 +256,7 @@ function initView(view: ViewData, renderHost: any, component: any, context: any) case NodeType.Provider: let componentView: ViewData; if (nodeDef.provider.component) { - componentView = createView(view.services, view, i, nodeDef.provider.component()); + componentView = createView(view.services, view, i, i, nodeDef.provider.component()); } nodeData = createProvider(view, nodeDef, componentView); break; @@ -243,7 +272,9 @@ function initView(view: ViewData, renderHost: any, component: any, context: any) export function checkNoChangesView(view: ViewData) { view.def.update(CheckNoChanges, view); execEmbeddedViewsAction(view, ViewAction.CheckNoChanges); + execContentQueriesAction(view, QueryAction.CheckNoChanges); execComponentViewsAction(view, ViewAction.CheckNoChanges); + updateViewQueries(view, QueryAction.CheckNoChanges); } const CheckNoChanges: NodeUpdater = { @@ -293,10 +324,12 @@ const CheckNoChanges: NodeUpdater = { export function checkAndUpdateView(view: ViewData) { view.def.update(CheckAndUpdate, view); execEmbeddedViewsAction(view, ViewAction.CheckAndUpdate); + execContentQueriesAction(view, QueryAction.CheckAndUpdate); callLifecycleHooksChildrenFirst( view, NodeFlags.AfterContentChecked | (view.firstChange ? NodeFlags.AfterContentInit : 0)); execComponentViewsAction(view, ViewAction.CheckAndUpdate); + updateViewQueries(view, QueryAction.CheckAndUpdate); callLifecycleHooksChildrenFirst( view, NodeFlags.AfterViewChecked | (view.firstChange ? NodeFlags.AfterViewInit : 0)); diff --git a/modules/@angular/core/src/view/view_attach.ts b/modules/@angular/core/src/view/view_attach.ts index 143330d173..8452de2285 100644 --- a/modules/@angular/core/src/view/view_attach.ts +++ b/modules/@angular/core/src/view/view_attach.ts @@ -7,18 +7,28 @@ */ import {NodeData, NodeFlags, ViewData} from './types'; +import {declaredViewContainer} from './util'; export function attachEmbeddedView(node: NodeData, viewIndex: number, view: ViewData) { let embeddedViews = node.elementOrText.embeddedViews; if (viewIndex == null) { viewIndex = embeddedViews.length; } - // perf: array.push is faster than array.splice! - if (viewIndex >= embeddedViews.length) { - embeddedViews.push(view); - } else { - embeddedViews.splice(viewIndex, 0, view); + addToArray(embeddedViews, viewIndex, view); + const dvc = declaredViewContainer(view); + if (dvc && dvc !== node) { + let projectedViews = dvc.elementOrText.projectedViews; + if (!projectedViews) { + projectedViews = dvc.elementOrText.projectedViews = []; + } + projectedViews.push(view); } + + for (let queryId in view.def.nodeMatchedQueries) { + dirtyParentQuery(queryId, view); + } + + // update rendering const prevView = viewIndex > 0 ? embeddedViews[viewIndex - 1] : null; const prevNode = prevView ? prevView.nodes[prevView.def.lastRootNode] : node; const prevRenderNode = prevNode.elementOrText.node; @@ -41,12 +51,19 @@ export function detachEmbeddedView(node: NodeData, viewIndex: number): ViewData viewIndex = embeddedViews.length; } const view = embeddedViews[viewIndex]; - // perf: array.pop is faster than array.splice! - if (viewIndex >= embeddedViews.length - 1) { - embeddedViews.pop(); - } else { - embeddedViews.splice(viewIndex, 1); + removeFromArray(embeddedViews, viewIndex); + + const dvc = declaredViewContainer(view); + if (dvc && dvc !== node) { + const projectedViews = dvc.elementOrText.projectedViews; + removeFromArray(projectedViews, projectedViews.indexOf(view)); } + + for (let queryId in view.def.nodeMatchedQueries) { + dirtyParentQuery(queryId, view); + } + + // update rendering if (view.renderer) { view.renderer.detachView(rootRenderNodes(view)); } else { @@ -59,6 +76,45 @@ export function detachEmbeddedView(node: NodeData, viewIndex: number): ViewData return view; } +function addToArray(arr: any[], index: number, value: any) { + // perf: array.push is faster than array.splice! + if (index >= arr.length) { + arr.push(value); + } else { + arr.splice(index, 0, value); + } +} + +function removeFromArray(arr: any[], index: number) { + // perf: array.pop is faster than array.splice! + if (index >= arr.length - 1) { + arr.pop(); + } else { + arr.splice(index, 1); + } +} + +function dirtyParentQuery(queryId: string, view: ViewData) { + let nodeIndex = view.parentIndex; + view = view.parent; + let providerIdx: number; + while (view) { + const nodeDef = view.def.nodes[nodeIndex]; + providerIdx = nodeDef.providerIndices[queryId]; + if (providerIdx != null) { + break; + } + nodeIndex = view.parentIndex; + view = view.parent; + } + if (!view) { + throw new Error( + `Illegal State: Tried to dirty parent query ${queryId} but the query could not be found!`); + } + const providerData = view.nodes[providerIdx].provider; + providerData.queries[queryId].setDirty(); +} + export function rootRenderNodes(view: ViewData): any[] { const renderNodes: any[] = []; collectSiblingRenderNodes(view, 0, renderNodes); diff --git a/modules/@angular/core/test/view/anchor_spec.ts b/modules/@angular/core/test/view/anchor_spec.ts index 76db0e02f0..b8bada556d 100644 --- a/modules/@angular/core/test/view/anchor_spec.ts +++ b/modules/@angular/core/test/view/anchor_spec.ts @@ -48,21 +48,21 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { describe('create', () => { it('should create anchor nodes without parents', () => { const rootNodes = - createAndGetRootNodes(compViewDef([anchorDef(NodeFlags.None, 0)])).rootNodes; + createAndGetRootNodes(compViewDef([anchorDef(NodeFlags.None, null, 0)])).rootNodes; expect(rootNodes.length).toBe(1); }); it('should create views with multiple root anchor nodes', () => { const rootNodes = createAndGetRootNodes(compViewDef([ - anchorDef(NodeFlags.None, 0), anchorDef(NodeFlags.None, 0) + anchorDef(NodeFlags.None, null, 0), anchorDef(NodeFlags.None, null, 0) ])).rootNodes; expect(rootNodes.length).toBe(2); }); it('should create anchor nodes with parents', () => { const rootNodes = createAndGetRootNodes(compViewDef([ - elementDef(NodeFlags.None, 1, 'div'), - anchorDef(NodeFlags.None, 0), + elementDef(NodeFlags.None, null, 1, 'div'), + anchorDef(NodeFlags.None, null, 0), ])).rootNodes; expect(getDOM().childNodes(rootNodes[0]).length).toBe(1); }); diff --git a/modules/@angular/core/test/view/component_view_spec.ts b/modules/@angular/core/test/view/component_view_spec.ts index 16e76c412c..39d5fa9de0 100644 --- a/modules/@angular/core/test/view/component_view_spec.ts +++ b/modules/@angular/core/test/view/component_view_spec.ts @@ -52,10 +52,12 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { } const {view, rootNodes} = createAndGetRootNodes(compViewDef([ - elementDef(NodeFlags.None, 1, 'div'), - providerDef(NodeFlags.None, AComp, [], null, null, () => compViewDef([ - elementDef(NodeFlags.None, 0, 'span'), - ])), + elementDef(NodeFlags.None, null, 1, 'div'), + providerDef( + NodeFlags.None, null, AComp, [], null, null, null, + () => compViewDef([ + elementDef(NodeFlags.None, null, 0, 'span'), + ])), ])); const compView = view.nodes[1].provider.componentView; @@ -78,10 +80,10 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { const {view, rootNodes} = createAndGetRootNodes( compViewDef([ - elementDef(NodeFlags.None, 1, 'div'), - providerDef(NodeFlags.None, AComp, [], null, null, () => compViewDef( + elementDef(NodeFlags.None, null, 1, 'div'), + providerDef(NodeFlags.None, null, AComp, [], null, null, null, () => compViewDef( [ - elementDef(NodeFlags.None, 0, 'span', null, [[BindingType.ElementAttribute, 'a', SecurityContext.NONE]]), + elementDef(NodeFlags.None, null, 0, 'span', null, [[BindingType.ElementAttribute, 'a', SecurityContext.NONE]]), ], update )), ], jasmine.createSpy('parentUpdater'))); @@ -114,12 +116,12 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { } const {view, rootNodes} = createAndGetRootNodes(compViewDef([ - elementDef(NodeFlags.None, 1, 'div'), + elementDef(NodeFlags.None, null, 1, 'div'), providerDef( - NodeFlags.None, AComp, [], null, null, + NodeFlags.None, null, AComp, [], null, null, null, () => compViewDef([ - elementDef(NodeFlags.None, 1, 'span'), - providerDef(NodeFlags.OnDestroy, ChildProvider, []) + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.OnDestroy, null, ChildProvider, []) ])), ])); diff --git a/modules/@angular/core/test/view/element_spec.ts b/modules/@angular/core/test/view/element_spec.ts index ddf8a1070c..81f52fe049 100644 --- a/modules/@angular/core/test/view/element_spec.ts +++ b/modules/@angular/core/test/view/element_spec.ts @@ -47,24 +47,25 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { describe('create', () => { it('should create elements without parents', () => { - const rootNodes = - createAndGetRootNodes(compViewDef([elementDef(NodeFlags.None, 0, 'span')])).rootNodes; + const rootNodes = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 0, 'span') + ])).rootNodes; expect(rootNodes.length).toBe(1); expect(getDOM().nodeName(rootNodes[0]).toLowerCase()).toBe('span'); }); it('should create views with multiple root elements', () => { - const rootNodes = - createAndGetRootNodes(compViewDef([ - elementDef(NodeFlags.None, 0, 'span'), elementDef(NodeFlags.None, 0, 'span') - ])).rootNodes; + const rootNodes = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 0, 'span'), + elementDef(NodeFlags.None, null, 0, 'span') + ])).rootNodes; expect(rootNodes.length).toBe(2); }); it('should create elements with parents', () => { const rootNodes = createAndGetRootNodes(compViewDef([ - elementDef(NodeFlags.None, 1, 'div'), - elementDef(NodeFlags.None, 0, 'span'), + elementDef(NodeFlags.None, null, 1, 'div'), + elementDef(NodeFlags.None, null, 0, 'span'), ])).rootNodes; expect(rootNodes.length).toBe(1); const spanEl = getDOM().childNodes(rootNodes[0])[0]; @@ -73,7 +74,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { it('should set fixed attributes', () => { const rootNodes = createAndGetRootNodes(compViewDef([ - elementDef(NodeFlags.None, 0, 'div', {'title': 'a'}), + elementDef(NodeFlags.None, null, 0, 'div', {'title': 'a'}), ])).rootNodes; expect(rootNodes.length).toBe(1); expect(getDOM().getAttribute(rootNodes[0], 'title')).toBe('a'); @@ -85,7 +86,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ elementDef( - NodeFlags.None, 0, 'div', null, + NodeFlags.None, null, 0, 'div', null, [[BindingType.ElementAttribute, 'a1', SecurityContext.NONE]]), ], (updater, view) => updater.checkInline(view, 0, attrValue))); @@ -114,7 +115,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ elementDef( - NodeFlags.None, 0, 'input', null, + NodeFlags.None, null, 0, 'input', null, [ [BindingType.ElementProperty, 'title', SecurityContext.NONE], [BindingType.ElementProperty, 'value', SecurityContext.NONE] @@ -145,7 +146,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ elementDef( - NodeFlags.None, 0, 'div', null, + NodeFlags.None, null, 0, 'div', null, [ [BindingType.ElementAttribute, 'a1', SecurityContext.NONE], [BindingType.ElementAttribute, 'a2', SecurityContext.NONE] @@ -176,7 +177,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ elementDef( - NodeFlags.None, 0, 'div', null, + NodeFlags.None, null, 0, 'div', null, [[BindingType.ElementClass, 'c1'], [BindingType.ElementClass, 'c2']]), ], config.updater)); @@ -204,7 +205,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ elementDef( - NodeFlags.None, 0, 'div', null, + NodeFlags.None, null, 0, 'div', null, [ [BindingType.ElementStyle, 'width', 'px'], [BindingType.ElementStyle, 'color', null] @@ -250,7 +251,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { const removeListenerSpy = spyOn(HTMLElement.prototype, 'removeEventListener').and.callThrough(); const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef( - [elementDef(NodeFlags.None, 0, 'button', null, null, ['click'])], null, + [elementDef(NodeFlags.None, null, 0, 'button', null, null, ['click'])], null, handleEventSpy)); rootNodes[0].click(); @@ -272,7 +273,8 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { const addListenerSpy = spyOn(window, 'addEventListener'); const removeListenerSpy = spyOn(window, 'removeEventListener'); const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef( - [elementDef(NodeFlags.None, 0, 'button', null, null, [['window', 'windowClick']])], + [elementDef( + NodeFlags.None, null, 0, 'button', null, null, [['window', 'windowClick']])], null, handleEventSpy)); expect(addListenerSpy).toHaveBeenCalled(); @@ -297,7 +299,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { const removeListenerSpy = spyOn(document, 'removeEventListener'); const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef( [elementDef( - NodeFlags.None, 0, 'button', null, null, [['document', 'documentClick']])], + NodeFlags.None, null, 0, 'button', null, null, [['document', 'documentClick']])], null, handleEventSpy)); expect(addListenerSpy).toHaveBeenCalled(); @@ -321,7 +323,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { let preventDefaultSpy: jasmine.Spy; const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef( - [elementDef(NodeFlags.None, 0, 'button', null, null, ['click'])], null, + [elementDef(NodeFlags.None, null, 0, 'button', null, null, ['click'])], null, (view, index, eventName, event) => { preventDefaultSpy = spyOn(event, 'preventDefault').and.callThrough(); return eventHandlerResult; diff --git a/modules/@angular/core/test/view/embedded_view_spec.ts b/modules/@angular/core/test/view/embedded_view_spec.ts index b7b014c752..6f461cc955 100644 --- a/modules/@angular/core/test/view/embedded_view_spec.ts +++ b/modules/@angular/core/test/view/embedded_view_spec.ts @@ -56,9 +56,9 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { const {view: parentView, rootNodes} = createAndGetRootNodes( compViewDef([ - elementDef(NodeFlags.None, 2, 'div'), - anchorDef(NodeFlags.HasEmbeddedViews, 0, embeddedViewDef([elementDef( - NodeFlags.None, 0, 'span')])), + elementDef(NodeFlags.None, null, 2, 'div'), + anchorDef(NodeFlags.HasEmbeddedViews, null, 0, embeddedViewDef([elementDef( + NodeFlags.None, null, 0, 'span')])), ]), parentContext); @@ -69,12 +69,13 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { it('should attach and detach embedded views', () => { const {view: parentView, rootNodes} = createAndGetRootNodes(compViewDef([ - elementDef(NodeFlags.None, 2, 'div'), + elementDef(NodeFlags.None, null, 2, 'div'), anchorDef( - NodeFlags.HasEmbeddedViews, 0, - embeddedViewDef([elementDef(NodeFlags.None, 0, 'span', {'name': 'child0'})])), - anchorDef(NodeFlags.None, 0, embeddedViewDef([elementDef( - NodeFlags.None, 0, 'span', {'name': 'child1'})])) + NodeFlags.HasEmbeddedViews, null, 0, + embeddedViewDef([elementDef(NodeFlags.None, null, 0, 'span', {'name': 'child0'})])), + anchorDef( + NodeFlags.None, null, 0, + embeddedViewDef([elementDef(NodeFlags.None, null, 0, 'span', {'name': 'child1'})])) ])); const childView0 = createEmbeddedView(parentView, parentView.def.nodes[1]); @@ -99,9 +100,9 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { it('should include embedded views in root nodes', () => { const {view: parentView} = createAndGetRootNodes(compViewDef([ anchorDef( - NodeFlags.HasEmbeddedViews, 0, - embeddedViewDef([elementDef(NodeFlags.None, 0, 'span', {'name': 'child0'})])), - elementDef(NodeFlags.None, 0, 'span', {'name': 'after'}) + NodeFlags.HasEmbeddedViews, null, 0, + embeddedViewDef([elementDef(NodeFlags.None, null, 0, 'span', {'name': 'child0'})])), + elementDef(NodeFlags.None, null, 0, 'span', {'name': 'after'}) ])); const childView0 = createEmbeddedView(parentView, parentView.def.nodes[0]); @@ -119,12 +120,12 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, childValue)); const {view: parentView, rootNodes} = createAndGetRootNodes(compViewDef([ - elementDef(NodeFlags.None, 1, 'div'), + elementDef(NodeFlags.None, null, 1, 'div'), anchorDef( - NodeFlags.HasEmbeddedViews, 0, + NodeFlags.HasEmbeddedViews, null, 0, embeddedViewDef( [elementDef( - NodeFlags.None, 0, 'span', null, + NodeFlags.None, null, 0, 'span', null, [[BindingType.ElementAttribute, 'name', SecurityContext.NONE]])], update)) ])); @@ -159,10 +160,10 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { } const {view: parentView, rootNodes} = createAndGetRootNodes(compViewDef([ - elementDef(NodeFlags.None, 1, 'div'), - anchorDef(NodeFlags.HasEmbeddedViews, 0, embeddedViewDef([ - elementDef(NodeFlags.None, 1, 'span'), - providerDef(NodeFlags.OnDestroy, ChildProvider, []) + elementDef(NodeFlags.None, null, 1, 'div'), + anchorDef(NodeFlags.HasEmbeddedViews, null, 0, embeddedViewDef([ + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.OnDestroy, null, ChildProvider, []) ])) ])); diff --git a/modules/@angular/core/test/view/provider_spec.ts b/modules/@angular/core/test/view/provider_spec.ts index 3c42070710..54fa910398 100644 --- a/modules/@angular/core/test/view/provider_spec.ts +++ b/modules/@angular/core/test/view/provider_spec.ts @@ -56,8 +56,10 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { constructor() { instances.push(this); } } - createAndGetRootNodes(compViewDef( - [elementDef(NodeFlags.None, 1, 'span'), providerDef(NodeFlags.None, SomeService, [])])); + createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.None, null, SomeService, []) + ])); expect(instances.length).toBe(1); }); @@ -74,8 +76,8 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { it('should inject deps from the same element', () => { createAndGetRootNodes(compViewDef([ - elementDef(NodeFlags.None, 2, 'span'), providerDef(NodeFlags.None, Dep, []), - providerDef(NodeFlags.None, SomeService, [Dep]) + elementDef(NodeFlags.None, null, 2, 'span'), providerDef(NodeFlags.None, null, Dep, []), + providerDef(NodeFlags.None, null, SomeService, [Dep]) ])); expect(instance.dep instanceof Dep).toBeTruthy(); @@ -83,8 +85,9 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { it('should inject deps from a parent element', () => { createAndGetRootNodes(compViewDef([ - elementDef(NodeFlags.None, 3, 'span'), providerDef(NodeFlags.None, Dep, []), - elementDef(NodeFlags.None, 1, 'span'), providerDef(NodeFlags.None, SomeService, [Dep]) + elementDef(NodeFlags.None, null, 3, 'span'), providerDef(NodeFlags.None, null, Dep, []), + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.None, null, SomeService, [Dep]) ])); expect(instance.dep instanceof Dep).toBeTruthy(); @@ -92,8 +95,9 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { it('should not inject deps from sibling root elements', () => { const nodes = [ - elementDef(NodeFlags.None, 1, 'span'), providerDef(NodeFlags.None, Dep, []), - elementDef(NodeFlags.None, 1, 'span'), providerDef(NodeFlags.None, SomeService, [Dep]) + elementDef(NodeFlags.None, null, 1, 'span'), providerDef(NodeFlags.None, null, Dep, []), + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.None, null, SomeService, [Dep]) ]; // root elements @@ -103,18 +107,18 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { // non root elements expect( () => createAndGetRootNodes( - compViewDef([elementDef(NodeFlags.None, 4, 'span')].concat(nodes)))) + compViewDef([elementDef(NodeFlags.None, null, 4, 'span')].concat(nodes)))) .toThrowError('No provider for Dep!'); }); it('should inject from a parent elment in a parent view', () => { createAndGetRootNodes(compViewDef([ - elementDef(NodeFlags.None, 1, 'div'), + elementDef(NodeFlags.None, null, 1, 'div'), providerDef( - NodeFlags.None, Dep, [], null, null, + NodeFlags.None, null, Dep, [], null, null, null, () => compViewDef([ - elementDef(NodeFlags.None, 1, 'span'), - providerDef(NodeFlags.None, SomeService, [Dep]) + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.None, null, SomeService, [Dep]) ])), ])); @@ -124,8 +128,8 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { describe('builtin tokens', () => { it('should inject ViewContainerRef', () => { createAndGetRootNodes(compViewDef([ - anchorDef(NodeFlags.HasEmbeddedViews, 1), - providerDef(NodeFlags.None, SomeService, [ViewContainerRef]) + anchorDef(NodeFlags.HasEmbeddedViews, null, 1), + providerDef(NodeFlags.None, null, SomeService, [ViewContainerRef]) ])); expect(instance.dep.createEmbeddedView).toBeTruthy(); @@ -133,8 +137,9 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { it('should inject TemplateRef', () => { createAndGetRootNodes(compViewDef([ - anchorDef(NodeFlags.None, 1, embeddedViewDef([anchorDef(NodeFlags.None, 0)])), - providerDef(NodeFlags.None, SomeService, [TemplateRef]) + anchorDef( + NodeFlags.None, null, 1, embeddedViewDef([anchorDef(NodeFlags.None, null, 0)])), + providerDef(NodeFlags.None, null, SomeService, [TemplateRef]) ])); expect(instance.dep.createEmbeddedView).toBeTruthy(); @@ -142,8 +147,8 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { it('should inject ElementRef', () => { createAndGetRootNodes(compViewDef([ - elementDef(NodeFlags.None, 1, 'span'), - providerDef(NodeFlags.None, SomeService, [ElementRef]) + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.None, null, SomeService, [ElementRef]) ])); expect(getDOM().nodeName(instance.dep.nativeElement).toLowerCase()).toBe('span'); @@ -152,16 +157,16 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { if (config.directDom) { it('should not inject Renderer when using directDom', () => { expect(() => createAndGetRootNodes(compViewDef([ - elementDef(NodeFlags.None, 1, 'span'), - providerDef(NodeFlags.None, SomeService, [Renderer]) + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.None, null, SomeService, [Renderer]) ]))) .toThrowError('No provider for Renderer!'); }); } else { it('should inject Renderer when not using directDom', () => { createAndGetRootNodes(compViewDef([ - elementDef(NodeFlags.None, 1, 'span'), - providerDef(NodeFlags.None, SomeService, [Renderer]) + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.None, null, SomeService, [Renderer]) ])); expect(instance.dep.createElement).toBeTruthy(); @@ -193,8 +198,8 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ - elementDef(NodeFlags.None, 1, 'span'), - providerDef(NodeFlags.None, SomeService, [], {a: [0, 'a'], b: [1, 'b']}) + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.None, null, SomeService, [], {a: [0, 'a'], b: [1, 'b']}) ], config.update)); @@ -213,8 +218,8 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { let propValue = 'v1'; const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ - elementDef(NodeFlags.None, 1, 'span'), - providerDef(NodeFlags.None, SomeService, [], {a: [0, 'a']}) + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.None, null, SomeService, [], {a: [0, 'a']}) ], (updater, view) => updater.checkInline(view, 1, propValue))); @@ -248,8 +253,8 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ - elementDef(NodeFlags.None, 1, 'span'), - providerDef(NodeFlags.None, SomeService, [], null, {emitter: 'someEventName'}) + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.None, null, SomeService, [], null, {emitter: 'someEventName'}) ], null, handleEvent)); @@ -286,10 +291,10 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { NodeFlags.AfterViewChecked | NodeFlags.OnDestroy; const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ - elementDef(NodeFlags.None, 3, 'span'), - providerDef(allFlags, SomeService, [], {a: [0, 'a']}), - elementDef(NodeFlags.None, 1, 'span'), - providerDef(allFlags, SomeService, [], {a: [0, 'a']}) + elementDef(NodeFlags.None, null, 3, 'span'), + providerDef(allFlags, null, SomeService, [], {a: [0, 'a']}), + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(allFlags, null, SomeService, [], {a: [0, 'a']}) ], (updater) => { updater.checkInline(view, 1, 'someValue'); @@ -345,8 +350,8 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ - elementDef(NodeFlags.None, 1, 'span'), - providerDef(NodeFlags.OnChanges, SomeService, [], {a: [0, 'nonMinifiedA']}) + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.OnChanges, null, SomeService, [], {a: [0, 'nonMinifiedA']}) ], (updater) => updater.checkInline(view, 1, currValue))); diff --git a/modules/@angular/core/test/view/pure_expression_spec.ts b/modules/@angular/core/test/view/pure_expression_spec.ts index 887cdaef5c..1550ed2e6a 100644 --- a/modules/@angular/core/test/view/pure_expression_spec.ts +++ b/modules/@angular/core/test/view/pure_expression_spec.ts @@ -45,8 +45,8 @@ export function main() { const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ - elementDef(NodeFlags.None, 2, 'span'), pureArrayDef(2), - providerDef(NodeFlags.None, Service, [], {data: [0, 'data']}) + elementDef(NodeFlags.None, null, 2, 'span'), pureArrayDef(2), + providerDef(NodeFlags.None, null, Service, [], {data: [0, 'data']}) ], (updater, view) => { callUpdater( @@ -79,8 +79,8 @@ export function main() { const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ - elementDef(NodeFlags.None, 2, 'span'), pureObjectDef(['a', 'b']), - providerDef(NodeFlags.None, Service, [], {data: [0, 'data']}) + elementDef(NodeFlags.None, null, 2, 'span'), pureObjectDef(['a', 'b']), + providerDef(NodeFlags.None, null, Service, [], {data: [0, 'data']}) ], (updater, view) => { callUpdater( @@ -117,9 +117,9 @@ export function main() { const {view, rootNodes} = createAndGetRootNodes(compViewDef( [ - elementDef(NodeFlags.None, 3, 'span'), providerDef(NodeFlags.None, SomePipe, []), - purePipeDef(SomePipe, 2), - providerDef(NodeFlags.None, Service, [], {data: [0, 'data']}) + elementDef(NodeFlags.None, null, 3, 'span'), + providerDef(NodeFlags.None, null, SomePipe, []), purePipeDef(SomePipe, 2), + providerDef(NodeFlags.None, null, Service, [], {data: [0, 'data']}) ], (updater, view) => { callUpdater( diff --git a/modules/@angular/core/test/view/query_spec.ts b/modules/@angular/core/test/view/query_spec.ts new file mode 100644 index 0000000000..5bab183c9b --- /dev/null +++ b/modules/@angular/core/test/view/query_spec.ts @@ -0,0 +1,390 @@ +/** + * @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 {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, attachEmbeddedView, checkAndUpdateView, checkNoChangesView, createEmbeddedView, createRootView, destroyView, detachEmbeddedView, elementDef, providerDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; +import {inject} from '@angular/core/testing'; +import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; + +export function main() { + describe(`Query Views`, () => { + 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 embeddedViewDef(nodes: NodeDef[], update?: ViewUpdateFn): ViewDefinition { + return viewDef(ViewFlags.None, nodes, update); + } + + function createAndGetRootNodes( + viewDef: ViewDefinition, context: any = null): {rootNodes: any[], view: ViewData} { + const view = createRootView(services, viewDef, context); + const rootNodes = rootRenderNodes(view); + return {rootNodes, view}; + } + + class AService {} + + class QueryService { + a: QueryList; + } + + function contentQueryProvider() { + return providerDef( + NodeFlags.None, null, QueryService, [], null, null, + {'a': ['query1', QueryBindingType.All]}); + } + + function aServiceProvider() { + return providerDef(NodeFlags.None, [['query1', QueryValueType.Provider]], AService, []); + } + + function viewQueryProvider(compView: ViewDefinition) { + return providerDef( + NodeFlags.None, null, QueryService, [], null, null, null, () => compView, + {'a': ['query1', QueryBindingType.All]}); + } + + describe('content queries', () => { + + it('should query providers on the same element and child elements', () => { + const {view} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 4, 'div'), + contentQueryProvider(), + aServiceProvider(), + elementDef(NodeFlags.None, null, 1, 'div'), + aServiceProvider(), + ])); + + const qs: QueryService = view.nodes[1].provider.instance; + expect(qs.a).toBeUndefined(); + + checkAndUpdateView(view); + + const as = qs.a.toArray(); + expect(as.length).toBe(2); + expect(as[0]).toBe(view.nodes[2].provider.instance); + expect(as[1]).toBe(view.nodes[4].provider.instance); + }); + + it('should not query providers on sibling or parent elements', () => { + const {view} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 5, 'div'), + aServiceProvider(), + elementDef(NodeFlags.None, null, 1, 'div'), + contentQueryProvider(), + elementDef(NodeFlags.None, null, 1, 'div'), + aServiceProvider(), + ])); + + checkAndUpdateView(view); + + const qs: QueryService = view.nodes[3].provider.instance; + expect(qs.a.length).toBe(0); + }); + }); + + describe('view queries', () => { + it('should query providers in the view', () => { + const {view} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 1, 'div'), + viewQueryProvider(compViewDef([ + elementDef(NodeFlags.None, null, 1, 'span'), + aServiceProvider(), + ])), + ])); + + checkAndUpdateView(view); + + const comp: QueryService = view.nodes[1].provider.instance; + const compView = view.nodes[1].provider.componentView; + expect(comp.a.length).toBe(1); + expect(comp.a.first).toBe(compView.nodes[1].provider.instance); + }); + + it('should not query providers on the host element', () => { + const {view} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 2, 'div'), + viewQueryProvider(compViewDef([ + elementDef(NodeFlags.None, null, 1, 'span'), + ])), + aServiceProvider(), + ])); + + checkAndUpdateView(view); + const comp: QueryService = view.nodes[1].provider.instance; + expect(comp.a.length).toBe(0); + }); + }); + + describe('embedded views', () => { + it('should query providers in embedded views', () => { + const {view} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 3, 'div'), + contentQueryProvider(), + anchorDef( + NodeFlags.HasEmbeddedViews, null, 1, viewDef( + ViewFlags.None, + [ + elementDef(NodeFlags.None, null, 1, 'div'), + aServiceProvider(), + ])), + contentQueryProvider(), + ])); + + const childView = createEmbeddedView(view, view.def.nodes[2]); + attachEmbeddedView(view.nodes[2], 0, childView); + checkAndUpdateView(view); + + // queries on parent elements of anchors + const qs1: QueryService = view.nodes[1].provider.instance; + expect(qs1.a.length).toBe(1); + expect(qs1.a.first instanceof AService).toBe(true); + + // queries on the anchor + const qs2: QueryService = view.nodes[3].provider.instance; + expect(qs2.a.length).toBe(1); + expect(qs2.a.first instanceof AService).toBe(true); + }); + + it('should query providers in embedded views only at the template declaration', () => { + const {view} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 2, 'div'), + contentQueryProvider(), + anchorDef( + NodeFlags.HasEmbeddedViews, null, 0, viewDef( + ViewFlags.None, + [ + elementDef(NodeFlags.None, null, 1, 'div'), + aServiceProvider(), + ])), + elementDef(NodeFlags.None, null, 2, 'div'), + contentQueryProvider(), + anchorDef(NodeFlags.HasEmbeddedViews, null, 0), + ])); + + const childView = createEmbeddedView(view, view.def.nodes[2]); + // attach at a different place than the one where the template was defined + attachEmbeddedView(view.nodes[5], 0, childView); + + checkAndUpdateView(view); + + // query on the declaration place + const qs1: QueryService = view.nodes[1].provider.instance; + expect(qs1.a.length).toBe(1); + expect(qs1.a.first instanceof AService).toBe(true); + + // query on the attach place + const qs2: QueryService = view.nodes[4].provider.instance; + expect(qs2.a.length).toBe(0); + }); + + it('should checkNoChanges', () => { + const {view} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 3, 'div'), + contentQueryProvider(), + 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[2]); + attachEmbeddedView(view.nodes[2], 0, childView); + + expect(() => checkNoChangesView(view)) + .toThrowError( + `Expression has changed after it was checked. Previous value: ''. Current value: '[object Object]'.`); + }); + + it('should update content queries if embedded views are added or removed', () => { + const {view} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 2, 'div'), + contentQueryProvider(), + anchorDef( + NodeFlags.HasEmbeddedViews, null, 0, viewDef( + ViewFlags.None, + [ + elementDef(NodeFlags.None, null, 1, 'div'), + aServiceProvider(), + ])), + ])); + + checkAndUpdateView(view); + + const qs: QueryService = view.nodes[1].provider.instance; + expect(qs.a.length).toBe(0); + + const childView = createEmbeddedView(view, view.def.nodes[2]); + attachEmbeddedView(view.nodes[2], 0, childView); + checkAndUpdateView(view); + + expect(qs.a.length).toBe(1); + + detachEmbeddedView(view.nodes[2], 0); + checkAndUpdateView(view); + + expect(qs.a.length).toBe(0); + }); + + it('should update view queries if embedded views are added or removed', () => { + const {view} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 1, 'div'), + viewQueryProvider(compViewDef([ + anchorDef( + NodeFlags.HasEmbeddedViews, null, 0, + viewDef( + ViewFlags.None, + [ + elementDef(NodeFlags.None, null, 1, 'div'), + aServiceProvider(), + ])), + ])), + ])); + + checkAndUpdateView(view); + + const comp: QueryService = view.nodes[1].provider.instance; + expect(comp.a.length).toBe(0); + + const compView = view.nodes[1].provider.componentView; + const childView = createEmbeddedView(compView, compView.def.nodes[0]); + attachEmbeddedView(compView.nodes[0], 0, childView); + checkAndUpdateView(view); + + expect(comp.a.length).toBe(1); + + detachEmbeddedView(compView.nodes[0], 0); + checkAndUpdateView(view); + + expect(comp.a.length).toBe(0); + }); + }); + + describe('QueryBindingType', () => { + it('should query all matches', () => { + class QueryService { + a: QueryList; + } + + const {view} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 3, 'div'), + providerDef( + NodeFlags.None, null, QueryService, [], null, null, + {'a': ['query1', QueryBindingType.All]}), + aServiceProvider(), + aServiceProvider(), + ])); + + checkAndUpdateView(view); + + const qs: QueryService = view.nodes[1].provider.instance; + expect(qs.a instanceof QueryList).toBeTruthy(); + expect(qs.a.toArray()).toEqual([ + view.nodes[2].provider.instance, + view.nodes[3].provider.instance, + ]); + }); + + it('should query the first match', () => { + class QueryService { + a: AService; + } + + const {view} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, null, 3, 'div'), + providerDef( + NodeFlags.None, null, QueryService, [], null, null, + {'a': ['query1', QueryBindingType.First]}), + aServiceProvider(), + aServiceProvider(), + ])); + + checkAndUpdateView(view); + + const qs: QueryService = view.nodes[1].provider.instance; + expect(qs.a).toBe(view.nodes[2].provider.instance); + }); + }); + + describe('query builtins', () => { + it('should query ElementRef', () => { + class QueryService { + a: ElementRef; + } + + const {view} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, [['query1', QueryValueType.ElementRef]], 1, 'div'), + providerDef( + NodeFlags.None, null, QueryService, [], null, null, + {'a': ['query1', QueryBindingType.First]}), + ])); + + checkAndUpdateView(view); + + const qs: QueryService = view.nodes[1].provider.instance; + expect(qs.a.nativeElement).toBe(view.nodes[0].elementOrText.node); + }); + + it('should query TemplateRef', () => { + class QueryService { + a: TemplateRef; + } + + const {view} = createAndGetRootNodes(compViewDef([ + anchorDef( + NodeFlags.None, [['query1', QueryValueType.TemplateRef]], 1, + viewDef(ViewFlags.None, [anchorDef(NodeFlags.None, null, 0)])), + providerDef( + NodeFlags.None, null, QueryService, [], null, null, + {'a': ['query1', QueryBindingType.First]}), + ])); + + checkAndUpdateView(view); + + const qs: QueryService = view.nodes[1].provider.instance; + expect(qs.a.createEmbeddedView).toBeTruthy(); + }); + + it('should query ViewContainerRef', () => { + class QueryService { + a: ViewContainerRef; + } + + const {view} = createAndGetRootNodes(compViewDef([ + anchorDef(NodeFlags.None, [['query1', QueryValueType.ViewContainerRef]], 1), + providerDef( + NodeFlags.None, null, QueryService, [], null, null, + {'a': ['query1', QueryBindingType.First]}), + ])); + + checkAndUpdateView(view); + + const qs: QueryService = view.nodes[1].provider.instance; + expect(qs.a.createEmbeddedView).toBeTruthy(); + }); + }); + }); +} \ No newline at end of file diff --git a/modules/@angular/core/test/view/text_spec.ts b/modules/@angular/core/test/view/text_spec.ts index 0dac5c5dc3..af2118c7ad 100644 --- a/modules/@angular/core/test/view/text_spec.ts +++ b/modules/@angular/core/test/view/text_spec.ts @@ -60,7 +60,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) { it('should create text nodes with parents', () => { const rootNodes = createAndGetRootNodes(compViewDef([ - elementDef(NodeFlags.None, 1, 'div'), + elementDef(NodeFlags.None, null, 1, 'div'), textDef(['a']), ])).rootNodes; expect(rootNodes.length).toBe(1); diff --git a/modules/@angular/core/test/view/view_def_spec.ts b/modules/@angular/core/test/view/view_def_spec.ts index 1df1104613..a2a0bc4030 100644 --- a/modules/@angular/core/test/view/view_def_spec.ts +++ b/modules/@angular/core/test/view/view_def_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {NodeFlags, NodeUpdater, ViewData, ViewDefinition, ViewFlags, anchorDef, checkAndUpdateView, checkNoChangesView, elementDef, providerDef, textDef, viewDef} from '@angular/core/src/view/index'; +import {NodeFlags, NodeUpdater, QueryValueType, ViewData, ViewDefinition, ViewFlags, anchorDef, checkAndUpdateView, checkNoChangesView, elementDef, providerDef, textDef, viewDef} from '@angular/core/src/view/index'; export function main() { describe('viewDef', () => { @@ -26,9 +26,9 @@ export function main() { it('should reverse child order for one level, one root', () => { const vd = viewDef(ViewFlags.None, [ - elementDef(NodeFlags.None, 2, 'span'), // level 0, index 0 - textDef(['a']), // level 1, index 1 - textDef(['a']), // level 1, index 2 + elementDef(NodeFlags.None, null, 2, 'span'), // level 0, index 0 + textDef(['a']), // level 1, index 1 + textDef(['a']), // level 1, index 2 ]); expect(reverseChildOrder(vd)).toEqual([0, 2, 1]); @@ -36,11 +36,11 @@ export function main() { it('should reverse child order for 1 level, 2 roots', () => { const vd = viewDef(ViewFlags.None, [ - elementDef(NodeFlags.None, 2, 'span'), // level 0, index 0 - textDef(['a']), // level 1, index 1 - textDef(['a']), // level 1, index 2 - elementDef(NodeFlags.None, 1, 'span'), // level 0, index 3 - textDef(['a']), // level 1, index 4 + elementDef(NodeFlags.None, null, 2, 'span'), // level 0, index 0 + textDef(['a']), // level 1, index 1 + textDef(['a']), // level 1, index 2 + elementDef(NodeFlags.None, null, 1, 'span'), // level 0, index 3 + textDef(['a']), // level 1, index 4 ]); expect(reverseChildOrder(vd)).toEqual([3, 4, 0, 2, 1]); @@ -48,11 +48,11 @@ export function main() { it('should reverse child order for 2 levels', () => { const vd = viewDef(ViewFlags.None, [ - elementDef(NodeFlags.None, 4, 'span'), // level 0, index 0 - elementDef(NodeFlags.None, 1, 'span'), // level 1, index 1 - textDef(['a']), // level 2, index 2 - elementDef(NodeFlags.None, 1, 'span'), // level 1, index 3 - textDef(['a']), // level 2, index 4 + elementDef(NodeFlags.None, null, 4, 'span'), // level 0, index 0 + elementDef(NodeFlags.None, null, 1, 'span'), // level 1, index 1 + textDef(['a']), // level 2, index 2 + elementDef(NodeFlags.None, null, 1, 'span'), // level 1, index 3 + textDef(['a']), // level 2, index 4 ]); expect(reverseChildOrder(vd)).toEqual([0, 3, 4, 1, 2]); @@ -60,14 +60,14 @@ export function main() { it('should reverse child order for mixed levels', () => { const vd = viewDef(ViewFlags.None, [ - textDef(['a']), // level 0, index 0 - elementDef(NodeFlags.None, 5, 'span'), // level 0, index 1 - textDef(['a']), // level 1, index 2 - elementDef(NodeFlags.None, 1, 'span'), // level 1, index 3 - textDef(['a']), // level 2, index 4 - elementDef(NodeFlags.None, 1, 'span'), // level 1, index 5 - textDef(['a']), // level 2, index 6 - textDef(['a']), // level 0, index 7 + textDef(['a']), // level 0, index 0 + elementDef(NodeFlags.None, null, 5, 'span'), // level 0, index 1 + textDef(['a']), // level 1, index 2 + elementDef(NodeFlags.None, null, 1, 'span'), // level 1, index 3 + textDef(['a']), // level 2, index 4 + elementDef(NodeFlags.None, null, 1, 'span'), // level 1, index 5 + textDef(['a']), // level 2, index 6 + textDef(['a']), // level 0, index 7 ]); expect(reverseChildOrder(vd)).toEqual([7, 1, 5, 6, 3, 4, 2, 0]); @@ -81,7 +81,7 @@ export function main() { it('should calculate parents for one level', () => { const vd = viewDef(ViewFlags.None, [ - elementDef(NodeFlags.None, 2, 'span'), + elementDef(NodeFlags.None, null, 2, 'span'), textDef(['a']), textDef(['a']), ]); @@ -91,9 +91,9 @@ export function main() { it('should calculate parents for one level, multiple roots', () => { const vd = viewDef(ViewFlags.None, [ - elementDef(NodeFlags.None, 1, 'span'), + elementDef(NodeFlags.None, null, 1, 'span'), textDef(['a']), - elementDef(NodeFlags.None, 1, 'span'), + elementDef(NodeFlags.None, null, 1, 'span'), textDef(['a']), textDef(['a']), ]); @@ -103,10 +103,10 @@ export function main() { it('should calculate parents for multiple levels', () => { const vd = viewDef(ViewFlags.None, [ - elementDef(NodeFlags.None, 2, 'span'), - elementDef(NodeFlags.None, 1, 'span'), + elementDef(NodeFlags.None, null, 2, 'span'), + elementDef(NodeFlags.None, null, 1, 'span'), textDef(['a']), - elementDef(NodeFlags.None, 1, 'span'), + elementDef(NodeFlags.None, null, 1, 'span'), textDef(['a']), textDef(['a']), ]); @@ -123,20 +123,31 @@ export function main() { it('should calculate childFlags for one level', () => { const vd = viewDef(ViewFlags.None, [ - elementDef(NodeFlags.None, 1, 'span'), - providerDef(NodeFlags.AfterContentChecked, AService, []) + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.AfterContentChecked, null, AService, []) ]); expect(childFlags(vd)).toEqual([NodeFlags.AfterContentChecked, NodeFlags.None]); }); + it('should calculate childFlags for two levels', () => { + const vd = viewDef(ViewFlags.None, [ + elementDef(NodeFlags.None, null, 2, 'span'), elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.AfterContentChecked, null, AService, []) + ]); + + expect(childFlags(vd)).toEqual([ + NodeFlags.AfterContentChecked, NodeFlags.AfterContentChecked, NodeFlags.None + ]); + }); + it('should calculate childFlags for one level, multiple roots', () => { const vd = viewDef(ViewFlags.None, [ - elementDef(NodeFlags.None, 1, 'span'), - providerDef(NodeFlags.AfterContentChecked, AService, []), - elementDef(NodeFlags.None, 2, 'span'), - providerDef(NodeFlags.AfterContentInit, AService, []), - providerDef(NodeFlags.AfterViewChecked, AService, []), + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.AfterContentChecked, null, AService, []), + elementDef(NodeFlags.None, null, 2, 'span'), + providerDef(NodeFlags.AfterContentInit, null, AService, []), + providerDef(NodeFlags.AfterViewChecked, null, AService, []), ]); expect(childFlags(vd)).toEqual([ @@ -147,12 +158,12 @@ export function main() { it('should calculate childFlags for multiple levels', () => { const vd = viewDef(ViewFlags.None, [ - elementDef(NodeFlags.None, 2, 'span'), - elementDef(NodeFlags.None, 1, 'span'), - providerDef(NodeFlags.AfterContentChecked, AService, []), - elementDef(NodeFlags.None, 2, 'span'), - providerDef(NodeFlags.AfterContentInit, AService, []), - providerDef(NodeFlags.AfterViewInit, AService, []), + elementDef(NodeFlags.None, null, 2, 'span'), + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.AfterContentChecked, null, AService, []), + elementDef(NodeFlags.None, null, 2, 'span'), + providerDef(NodeFlags.AfterContentInit, null, AService, []), + providerDef(NodeFlags.AfterViewInit, null, AService, []), ]); expect(childFlags(vd)).toEqual([ @@ -161,6 +172,71 @@ export function main() { ]); }); }); + + describe('childMatchedQueries', () => { + function childMatchedQueries(viewDef: ViewDefinition): string[][] { + return viewDef.nodes.map(node => Object.keys(node.childMatchedQueries).sort()); + } + + it('should calculate childMatchedQueries for one level', () => { + const vd = viewDef(ViewFlags.None, [ + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.None, [['q1', QueryValueType.Provider]], AService, []) + ]); + + expect(childMatchedQueries(vd)).toEqual([['q1'], []]); + }); + + it('should calculate childMatchedQueries for two levels', () => { + const vd = viewDef(ViewFlags.None, [ + elementDef(NodeFlags.None, null, 2, 'span'), elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.None, [['q1', QueryValueType.Provider]], AService, []) + ]); + + expect(childMatchedQueries(vd)).toEqual([['q1'], ['q1'], []]); + }); + + it('should calculate childMatchedQueries for one level, multiple roots', () => { + const vd = viewDef(ViewFlags.None, [ + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.None, [['q1', QueryValueType.Provider]], AService, []), + elementDef(NodeFlags.None, null, 2, 'span'), + providerDef(NodeFlags.None, [['q2', QueryValueType.Provider]], AService, []), + providerDef(NodeFlags.None, [['q3', QueryValueType.Provider]], AService, []), + ]); + + expect(childMatchedQueries(vd)).toEqual([['q1'], [], ['q2', 'q3'], [], []]); + }); + + it('should calculate childMatchedQueries for multiple levels', () => { + const vd = viewDef(ViewFlags.None, [ + elementDef(NodeFlags.None, null, 2, 'span'), + elementDef(NodeFlags.None, null, 1, 'span'), + providerDef(NodeFlags.None, [['q1', QueryValueType.Provider]], AService, []), + elementDef(NodeFlags.None, null, 2, 'span'), + providerDef(NodeFlags.None, [['q2', QueryValueType.Provider]], AService, []), + providerDef(NodeFlags.None, [['q3', QueryValueType.Provider]], AService, []), + ]); + + expect(childMatchedQueries(vd)).toEqual([['q1'], ['q1'], [], ['q2', 'q3'], [], []]); + }); + + it('should included embedded views into childMatchedQueries', () => { + const vd = viewDef(ViewFlags.None, [ + elementDef(NodeFlags.None, null, 1, 'span'), + anchorDef( + NodeFlags.None, null, 0, + viewDef( + ViewFlags.None, + [ + elementDef(NodeFlags.None, [['q1', QueryValueType.Provider]], 1, 'span'), + ])) + ]); + + // Note: the template will become a sibling to the anchor once stamped out, + expect(childMatchedQueries(vd)).toEqual([['q1'], []]); + }); + }); }); } diff --git a/modules/benchmarks/src/tree/ng2_next/tree.ts b/modules/benchmarks/src/tree/ng2_next/tree.ts index 3c1e5504c7..29146f6932 100644 --- a/modules/benchmarks/src/tree/ng2_next/tree.ts +++ b/modules/benchmarks/src/tree/ng2_next/tree.ts @@ -24,16 +24,17 @@ export class TreeComponent { let viewFlags = ViewFlags.DirectDom; const TreeComponent_Host: ViewDefinition = viewDef(viewFlags, [ - elementDef(NodeFlags.None, 1, 'tree'), - providerDef(NodeFlags.None, TreeComponent, [], null, null, () => TreeComponent_0), + elementDef(NodeFlags.None, null, 1, 'tree'), + providerDef(NodeFlags.None, null, TreeComponent, [], null, null, null, () => TreeComponent_0), ]); const TreeComponent_1: ViewDefinition = viewDef( viewFlags, [ - elementDef(NodeFlags.None, 1, 'tree'), + elementDef(NodeFlags.None, null, 1, 'tree'), providerDef( - NodeFlags.None, TreeComponent, [], {data: [0, 'data']}, null, () => TreeComponent_0), + NodeFlags.None, null, TreeComponent, [], {data: [0, 'data']}, null, null, + () => TreeComponent_0), ], (updater: NodeUpdater, view: ViewData) => { const cmp = view.component; @@ -43,9 +44,10 @@ const TreeComponent_1: ViewDefinition = viewDef( const TreeComponent_2: ViewDefinition = viewDef( viewFlags, [ - elementDef(NodeFlags.None, 1, 'tree'), + elementDef(NodeFlags.None, null, 1, 'tree'), providerDef( - NodeFlags.None, TreeComponent, [], {data: [0, 'data']}, null, () => TreeComponent_0), + NodeFlags.None, null, TreeComponent, [], {data: [0, 'data']}, null, null, + () => TreeComponent_0), ], (updater: NodeUpdater, view: ViewData) => { const cmp = view.component; @@ -56,12 +58,13 @@ const TreeComponent_0: ViewDefinition = viewDef( viewFlags, [ elementDef( - NodeFlags.None, 1, 'span', null, [[BindingType.ElementStyle, 'backgroundColor', null]]), + NodeFlags.None, null, 1, 'span', null, + [[BindingType.ElementStyle, 'backgroundColor', null]]), textDef([' ', ' ']), - anchorDef(NodeFlags.HasEmbeddedViews, 1, TreeComponent_1), - providerDef(NodeFlags.None, NgIf, [ViewContainerRef, TemplateRef], {ngIf: [0, 'ngIf']}), - anchorDef(NodeFlags.HasEmbeddedViews, 1, TreeComponent_2), - providerDef(NodeFlags.None, NgIf, [ViewContainerRef, TemplateRef], {ngIf: [0, 'ngIf']}), + anchorDef(NodeFlags.HasEmbeddedViews, null, 1, TreeComponent_1), + providerDef(NodeFlags.None, null, NgIf, [ViewContainerRef, TemplateRef], {ngIf: [0, 'ngIf']}), + anchorDef(NodeFlags.HasEmbeddedViews, null, 1, TreeComponent_2), + providerDef(NodeFlags.None, null, NgIf, [ViewContainerRef, TemplateRef], {ngIf: [0, 'ngIf']}), ], (updater: NodeUpdater, view: ViewData) => { const cmp = view.component;