diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 8ec317a768..5f7673da65 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -10,7 +10,7 @@ import './ng_dev_mode'; import {Type} from '../core'; import {assertEqual, assertLessThan, assertNotEqual, assertNotNull} from './assert'; -import {CSSSelector, ContainerState, InitialInputData, InitialInputs, LContainer, LElement, LNode, LNodeFlags, LNodeInjector, LProjection, LText, LView, MinificationData, MinificationDataValue, NodeBindings, ProjectionState, QueryState, ViewState} from './interfaces'; +import {CSSSelector, ContainerState, InitialInputData, InitialInputs, LContainer, LContainerStatic, LElement, LNode, LNodeFlags, LNodeInjector, LNodeStatic, LProjection, LText, LView, MinificationData, MinificationDataValue, ProjectionState, QueryState, ViewState} from './interfaces'; import {assertNodeType} from './node_assert'; import {appendChild, insertChild, insertView, processProjectedNode, removeView} from './node_manipulation'; import {isNodeMatchingSelector} from './node_selector_matcher'; @@ -45,10 +45,10 @@ let isParent: boolean; * given type). * * Each node's static data is stored at the same index that it's stored - * in the nodes array. Any nodes that do not have static data store a null + * in the data array. Any nodes that do not have static data store a null * value to avoid a sparse array. */ -let ngData: (NodeBindings | null)[]; +let ngStaticData: (LNodeStatic | null)[]; /** * State of the current view being processed. @@ -63,17 +63,10 @@ let currentQuery: QueryState|null; let creationMode: boolean; /** - * An array of nodes (text, element, container, etc) and their bindings - * in the current view + * An array of nodes (text, element, container, etc), their bindings, and + * any local variables that need to be stored between invocations. */ -let nodesAndBindings: any[]; - -/** - * At times it is necessary for template to store information between invocations. - * `locals` is the storage mechanism along with `memory` instruction. - * For Example: storing queries between template invocations. - */ -let locals: any[]|null; +let data: any[]; /** * An array of directives in the current view @@ -125,16 +118,15 @@ let cleanup: any[]|null; export function enterView(newViewState: ViewState, host: LElement | LView | null): ViewState { const oldViewState = currentView; directives = newViewState.directives; - nodesAndBindings = newViewState.nodesAndBindings; + data = newViewState.data; bindingIndex = newViewState.bindingStartIndex || 0; - if (creationMode = !nodesAndBindings) { - // Absence of nodes implies creationMode. - (newViewState as{nodesAndBindings: LNode[]}).nodesAndBindings = nodesAndBindings = []; + if (creationMode = !data) { + // Absence of data implies creationMode. + (newViewState as{data: any[]}).data = data = []; } cleanup = newViewState.cleanup; renderer = newViewState.renderer; - locals = newViewState.locals; if (host != null) { previousOrParentNode = host; @@ -150,13 +142,12 @@ export const leaveView: (newViewState: ViewState) => void = enterView as any; export function createViewState(viewId: number, renderer: Renderer3): ViewState { const newView = { parent: currentView, - id: viewId, // -1 for component views - node: null !, // until we initialize it in createNode. - nodesAndBindings: null !, // Hack use as a marker for creationMode + id: viewId, // -1 for component views + node: null !, // until we initialize it in createNode. + data: null !, // Hack use as a marker for creationMode directives: [], cleanup: null, renderer: renderer, - locals: null, child: null, tail: null, next: null, @@ -170,18 +161,18 @@ export function createViewState(viewId: number, renderer: Renderer3): ViewState * A common way of creating the LNode to make sure that all of them have same shape to * keep the execution code monomorphic and fast. */ -export function createNode( +export function createLNode( index: number | null, type: LNodeFlags.Element, native: RElement | RText | null, viewState?: ViewState | null): LElement; -export function createNode( +export function createLNode( index: null, type: LNodeFlags.View, native: null, viewState: ViewState): LView; -export function createNode( +export function createLNode( index: number, type: LNodeFlags.Container, native: RComment, containerState: ContainerState): LContainer; -export function createNode( +export function createLNode( index: number, type: LNodeFlags.Projection, native: null, projectionState: ProjectionState): LProjection; -export function createNode( +export function createLNode( index: number | null, type: LNodeFlags, native: RText | RElement | RComment | null, state?: null | ViewState | ContainerState | ProjectionState): LElement<ext&LView&LContainer& LProjection { @@ -200,7 +191,7 @@ export function createNode( nodeInjector: parent ? parent.nodeInjector : null, data: isState ? state as any : null, query: query, - nodeBindings: null + staticData: null }; if ((type & LNodeFlags.ViewOrElement) === LNodeFlags.ViewOrElement && isState) { @@ -211,21 +202,21 @@ export function createNode( } if (index != null) { // We are Element or Container - ngDevMode && - assertEqual(nodesAndBindings.length, index, 'nodesAndBindings.length not in sequence'); - nodesAndBindings[index] = node; + ngDevMode && assertEqual(data.length, index, 'data.length not in sequence'); + data[index] = node; - // Every node adds a value to the data array to avoid a sparse array - if (ngData && index >= ngData.length) { - ngData[index] = null; - } else if (ngData) { - node.nodeBindings = ngData[index]; + // Every node adds a value to the static data array to avoid a sparse array + if (ngStaticData && index >= ngStaticData.length) { + ngStaticData[index] = null; + } else if (ngStaticData) { + node.staticData = ngStaticData[index]; } // Now link ourselves into the tree. if (isParent) { currentQuery = null; - if (previousOrParentNode.view === currentView) { + if (previousOrParentNode.view === currentView || + (previousOrParentNode.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.View) { // We are in the same view, which means we are adding content node to the parent View. ngDevMode && assertEqual(previousOrParentNode.child, null, 'previousNode.child'); previousOrParentNode.child = node; @@ -259,7 +250,7 @@ export function renderTemplate(host: LElement, template: ComponentTemplate ngDevMode && assertNotEqual(hostView, null, 'hostView'); const oldView = enterView(hostView, host); try { - ngData = template.ngData || (template.ngData = [] as never); + ngStaticData = template.ngStaticData || (template.ngStaticData = [] as never); template(context, creationMode); } finally { leaveView(oldView); @@ -327,7 +318,7 @@ export function getOrCreateNodeInjector(): LNodeInjector { /** * Create DOM element. The instruction must later be followed by `elementEnd()` call. * - * @param index Index of the element in the nodes array + * @param index Index of the element in the data array * @param nameOrComponentDef Name of the DOM Node or `ComponentDef`. * @param attrs Statically bound set of attributes to be written into the DOM element on creation. * @@ -342,7 +333,7 @@ export function elementCreate( if (nameOrComponentDef == null) { // native node retrieval - used for exporting elements as tpl local variables (
) - const node = nodesAndBindings[index] !; + const node = data[index] !; native = node && (node as LElement).native; } else { ngDevMode && assertEqual(currentView.bindingStartIndex, null, 'bindingStartIndex'); @@ -356,12 +347,12 @@ export function elementCreate( native = renderer.createElement(name); // Only component views should be added to the view tree directly. Embedded views are // accessed through their containers because they may be removed / re-added later. - node = createNode( + node = createLNode( index, LNodeFlags.Element, native, isHostElement ? addToViewTree(createViewState(-1, renderer)) : null); - if (node.nodeBindings == null) { - node.nodeBindings = ngData[index] = createNodeBindings(name, attrs || null); + if (node.staticData == null) { + node.staticData = ngStaticData[index] = createStaticData(name, attrs || null, null); } if (attrs) setUpAttributes(native, attrs); @@ -391,7 +382,7 @@ export function createError(text: string, token: any) { * @param elementOrSelector Render element or CSS selector to locate the element. */ export function elementHost(elementOrSelector: RElement | string) { - ngDevMode && assertNodesInRange(-1); + ngDevMode && assertDataInRange(-1); const rNode = typeof elementOrSelector === 'string' ? ((renderer as Renderer3Fn).selectRootElement ? (renderer as Renderer3Fn).selectRootElement(elementOrSelector) : @@ -404,7 +395,7 @@ export function elementHost(elementOrSelector: RElement | string) { throw createError('Host node is required:', elementOrSelector); } } - createNode(0, LNodeFlags.Element, rNode, createViewState(-1, renderer)); + createLNode(0, LNodeFlags.Element, rNode, createViewState(-1, renderer)); } @@ -434,9 +425,9 @@ export function listenerCreate( (cleanup || (cleanup = currentView.cleanup = [])).push(eventName, native, listener, useCapture); } - let mergeData: NodeBindings|null = node.nodeBindings !; + let mergeData: LNodeStatic|null = node.staticData !; if (mergeData.outputs === undefined) { - // if we createNodeBindings here, inputs must be undefined so we know they still need to be + // if we create LNodeStatic here, inputs must be undefined so we know they still need to be // checked mergeData.outputs = null; mergeData = generateMinifiedData(node.flags, mergeData); @@ -480,14 +471,14 @@ export function elementEnd() { /** * Update an attribute on an Element. This is used with a `bind` instruction. * - * @param index The index of the element to update in the nodes array + * @param index The index of the element to update in the data array * @param attrName Name of attribute. Because it is going to DOM, this is not subject to * renaming as port of minification. * @param value Value to write. This value will go through stringification. */ export function elementAttribute(index: number, attrName: string, value: any): void { if (value !== NO_CHANGE) { - const lElement = nodesAndBindings[index] as LElement; + const lElement = data[index] as LElement; if (value == null) { (renderer as Renderer3Fn).removeAttribute ? (renderer as Renderer3Fn).removeAttribute(lElement.native, attrName) : @@ -509,7 +500,7 @@ export function elementAttribute(index: number, attrName: string, value: any): v * be conducted at runtime as well so child components that add new @Inputs don't have to be * re-compiled. * - * @param index The index of the element to update in the nodes array + * @param index The index of the element to update in the data array * @param propName Name of property. Because it is going to DOM, this is not subject to * renaming as part of minification. * @param value New value to write. @@ -517,18 +508,19 @@ export function elementAttribute(index: number, attrName: string, value: any): v export function elementProperty(index: number, propName: string, value: T | NO_CHANGE): void { if (value === NO_CHANGE) return; - const node = nodesAndBindings[index] as LElement; + const node = data[index] as LElement; - let data: NodeBindings|null = node.nodeBindings !; - // if data.inputs is undefined, a listener has created output data, but inputs haven't yet been + let staticData: LNodeStatic|null = node.staticData !; + // if staticData.inputs is undefined, a listener has created output staticData, but inputs haven't + // yet been // checked - if (data.inputs === undefined) { + if (staticData.inputs === undefined) { // mark inputs as checked - data.inputs = null; - data = generateMinifiedData(node.flags, data, true); + staticData.inputs = null; + staticData = generateMinifiedData(node.flags, staticData, true); } - const inputData = data.inputs; + const inputData = staticData.inputs; let dataValue: MinificationDataValue|null; if (inputData && (dataValue = inputData[propName])) { setInputsForProperty(dataValue, value); @@ -541,8 +533,16 @@ export function elementProperty(index: number, propName: string, value: T | N } } -function createNodeBindings(tagName: string, attrs: string[] | null): NodeBindings { - return {tagName, attrs, initialInputs: undefined, inputs: undefined, outputs: undefined}; +function createStaticData( + tagName: string | null, attrs: string[] | null, + containerStatic: (LNodeStatic | null)[][] | null): LNodeStatic { + return { + tagName, + attrs, + initialInputs: undefined, + inputs: undefined, + outputs: undefined, containerStatic + }; } /** @@ -558,13 +558,12 @@ function setInputsForProperty(inputs: (number | string)[], value: any): void { /** * This function consolidates all the inputs or outputs defined by directives - * on this node into one object and stores it in ngData so it can + * on this node into one object and stores it in ngStaticData so it can * be shared between all templates of this type. * - * @param index Index where data should be stored in ngData + * @param index Index where data should be stored in ngStaticData */ -function generateMinifiedData( - flags: number, data: NodeBindings, isInputData = false): NodeBindings { +function generateMinifiedData(flags: number, data: LNodeStatic, isInputData = false): LNodeStatic { const start = flags >> LNodeFlags.INDX_SHIFT; const size = (flags & LNodeFlags.SIZE_MASK) >> LNodeFlags.SIZE_SHIFT; @@ -591,14 +590,14 @@ function generateMinifiedData( * * This instruction is meant to handle the [class.foo]="exp" case * - * @param index The index of the element to update in the nodes array + * @param index The index of the element to update in the data array * @param className Name of class to toggle. Because it is going to DOM, this is not subject to * renaming as part of minification. * @param value A value indicating if a given class should be added or removed. */ export function elementClass(index: number, className: string, value: T | NO_CHANGE): void { if (value !== NO_CHANGE) { - const lElement = nodesAndBindings[index] as LElement; + const lElement = data[index] as LElement; if (value) { (renderer as Renderer3Fn).addClass ? (renderer as Renderer3Fn).addClass(lElement.native, className) : @@ -615,7 +614,7 @@ export function elementClass(index: number, className: string, value: T | NO_ /** * Update a given style on an Element. * - * @param index Index of the element to change in the nodes array + * @param index Index of the element to change in the data array * @param styleName Name of property. Because it is going to DOM this is not subject to * renaming as part of minification. * @param value New value to write (null to remove). @@ -624,7 +623,7 @@ export function elementClass(index: number, className: string, value: T | NO_ export function elementStyle( index: number, styleName: string, value: T | NO_CHANGE, suffix?: string): void { if (value !== NO_CHANGE) { - const lElement = nodesAndBindings[index] as LElement; + const lElement = data[index] as LElement; if (value == null) { (renderer as Renderer3Fn).removeStyle ? (renderer as Renderer3Fn) @@ -651,7 +650,7 @@ export function elementStyle( /** * Create static text node * - * @param index Index of the node in the nodes array. + * @param index Index of the node in the data array. * @param value Value to write. This value will be stringified. * If value is not provided than the actual creation of the text node is delayed. */ @@ -662,7 +661,7 @@ export function textCreate(index: number, value?: any): void { (renderer as Renderer3Fn).createText(stringify(value)) : (renderer as Renderer3oo).createTextNode !(stringify(value))) : null; - const node = createNode(index, LNodeFlags.Element, textNode); + const node = createLNode(index, LNodeFlags.Element, textNode); // Text nodes are self closing. isParent = false; appendChild(node.parent !, textNode, currentView); @@ -672,12 +671,12 @@ export function textCreate(index: number, value?: any): void { * Create text node with binding * Bindings should be handled externally with the proper bind(1-8) method * - * @param index Index of the node in the nodes array. + * @param index Index of the node in the data array. * @param value Stringified value to write. */ export function textCreateBound(index: number, value: T | NO_CHANGE): void { // TODO(misko): I don't think index < nodes.length check is needed here. - let existingNode = index < nodesAndBindings.length && nodesAndBindings[index] as LText; + let existingNode = index < data.length && data[index] as LText; if (existingNode && existingNode.native) { // If DOM node exists and value changed, update textContent value !== NO_CHANGE && @@ -744,7 +743,7 @@ export function directiveCreate( if (diPublic) { diPublic(directiveDef !); } - const nodeBindings: NodeBindings|null = ngData && ngData[nodesAndBindings.length - 1]; + const nodeBindings: LNodeStatic|null = previousOrParentNode.staticData; if (nodeBindings && nodeBindings.attrs) setInputsFromAttrs(instance, directiveDef !.inputs, nodeBindings); } @@ -756,16 +755,16 @@ export function directiveCreate( * * @param instance Instance of the directive on which to set the initial inputs * @param inputs The list of inputs from the directive def - * @param nodeBindings The static data for this node + * @param staticData The static data for this node */ function setInputsFromAttrs( - instance: T, inputs: {[key: string]: string}, nodeBindings: NodeBindings): void { + instance: T, inputs: {[key: string]: string}, staticData: LNodeStatic): void { const directiveIndex = ((previousOrParentNode.flags & LNodeFlags.SIZE_MASK) >> LNodeFlags.SIZE_SHIFT) - 1; - let initialInputData = nodeBindings.initialInputs as InitialInputData | undefined; + let initialInputData = staticData.initialInputs as InitialInputData | undefined; if (initialInputData === undefined || directiveIndex >= initialInputData.length) { - initialInputData = generateInitialInputs(directiveIndex, inputs, nodeBindings); + initialInputData = generateInitialInputs(directiveIndex, inputs, staticData); } const initialInputs: InitialInputs|null = initialInputData[directiveIndex]; @@ -785,12 +784,12 @@ function setInputsFromAttrs( */ function generateInitialInputs( directiveIndex: number, inputs: {[key: string]: string}, - nodeBindings: NodeBindings): InitialInputData { + staticData: LNodeStatic): InitialInputData { const initialInputData: InitialInputData = - nodeBindings.initialInputs || (nodeBindings.initialInputs = []); + staticData.initialInputs || (staticData.initialInputs = []); initialInputData[directiveIndex] = null; - const attrs = nodeBindings.attrs !; + const attrs = staticData.attrs !; for (let i = 0; i < attrs.length; i += 2) { const attrName = attrs[i]; const minifiedInputName = inputs[attrName]; @@ -830,7 +829,7 @@ export function directiveLifeCycle( * * Only `LView`s can go into `LContainer`. * - * @param index The index of the container in the nodes array + * @param index The index of the container in the data array * @param template Optional inline template */ export function containerCreate( @@ -851,7 +850,7 @@ export function containerCreate( renderParent = currentParent as LElement; } - const node = createNode(index, LNodeFlags.Container, comment, { + const node = createLNode(index, LNodeFlags.Container, comment, { children: [], nextIndex: 0, renderParent, template: template == null ? null : template, @@ -859,8 +858,8 @@ export function containerCreate( parent: currentView }); - if (tagName && node.nodeBindings == null) { - node.nodeBindings = ngData[index] = createNodeBindings(tagName, attrs || null); + if (node.staticData == null) { + node.staticData = ngStaticData[index] = createStaticData(tagName || null, attrs || null, []); } // Containers are added to the current view tree instead of their embedded views @@ -881,11 +880,11 @@ export function containerEnd() { /** * Sets a container up to receive views. * - * @param index The index of the container in the nodes array + * @param index The index of the container in the data array */ export function refreshContainer(index: number): void { - ngDevMode && assertNodesInRange(index); - previousOrParentNode = nodesAndBindings[index] as LNode; + ngDevMode && assertDataInRange(index); + previousOrParentNode = data[index] as LNode; ngDevMode && assertNodeType(previousOrParentNode, LNodeFlags.Container); isParent = true; (previousOrParentNode as LContainer).data.nextIndex = 0; @@ -938,12 +937,32 @@ export function viewCreate(viewBlockId: number): boolean { } else { // When we create a new View, we always reset the state of the instructions. const newViewState = createViewState(viewBlockId, renderer); - enterView(newViewState, createNode(null, LNodeFlags.View, null, newViewState)); + enterView(newViewState, createLNode(null, LNodeFlags.View, null, newViewState)); containerState.nextIndex++; } + setNgStaticDataForView(viewBlockId); + return !viewUpdateMode; } +/** + * Each embedded view needs to set the global ngStaticData variable to the static data for that + * view. + * Otherwise, the view's static data for a particular node would overwrite the static + * data for a node in the view above it with the same index (since it's in the same template). + * + * @param viewIndex The index of the view's static data in containerStatic + */ +function setNgStaticDataForView(viewIndex: number): void { + ngDevMode && assertNodeType(previousOrParentNode.parent !, LNodeFlags.Container); + const containerStatic = + (previousOrParentNode.parent !.staticData as LContainerStatic).containerStatic; + if (viewIndex >= containerStatic.length || containerStatic[viewIndex] == null) { + containerStatic[viewIndex] = []; + } + ngStaticData = containerStatic[viewIndex]; +} + /** * Marks the end of the LView. */ @@ -975,8 +994,8 @@ export const refreshComponent: void = function( this: undefined | {template: ComponentTemplate}, directiveIndex: number, elementIndex: number, template: ComponentTemplate) { - ngDevMode && assertNodesInRange(elementIndex); - const element = nodesAndBindings ![elementIndex] as LElement; + ngDevMode && assertDataInRange(elementIndex); + const element = data ![elementIndex] as LElement; ngDevMode && assertNodeType(element, LNodeFlags.Element); ngDevMode && assertNotEqual(element.data, null, 'isComponent'); ngDevMode && assertDirectivesInRange(directiveIndex << 1); @@ -984,13 +1003,13 @@ export const refreshComponent: ngDevMode && assertNotEqual(hostView, null, 'hostView'); const directive = directives[directiveIndex << 1]; const oldView = enterView(hostView, element); - const oldNgData = ngData; + const oldNgStaticData = ngStaticData; try { const _template = template || this !.template; - ngData = _template.ngData || (_template.ngData = [] as never); + ngStaticData = _template.ngStaticData || (_template.ngStaticData = [] as never); _template(directive, creationMode); } finally { - ngData = oldNgData; + ngStaticData = oldNgStaticData; leaveView(oldView); } }; @@ -1023,9 +1042,9 @@ export function distributeProjectedNodes(selectors?: CSSSelector[]): LNode[][] { // - elements, excluding text nodes; // - containers that have tagName and attributes associated. - if (componentChild.nodeBindings) { + if (componentChild.staticData) { for (let i = 0; i < selectors !.length; i++) { - if (isNodeMatchingSelector(componentChild.nodeBindings, selectors ![i])) { + if (isNodeMatchingSelector(componentChild.staticData, selectors ![i])) { distributedNodes[i + 1].push(componentChild); break; // first matching selector "captures" a given node } else { @@ -1058,7 +1077,7 @@ export function distributeProjectedNodes(selectors?: CSSSelector[]): LNode[][] { export function contentProjection( nodeIndex: number, localIndex: number, selectorIndex: number = 0): void { const projectedNodes: ProjectionState = []; - const node = createNode(nodeIndex, LNodeFlags.Projection, null, projectedNodes); + const node = createLNode(nodeIndex, LNodeFlags.Projection, null, projectedNodes); isParent = false; // self closing const currentParent = node.parent; @@ -1066,9 +1085,8 @@ export function contentProjection( const componentNode = findComponentHost(currentView); // make sure that nodes to project were memorized - ngDevMode && assertNotNull(componentNode.data !.locals, 'componentNode.data.locals'); const nodesForSelector = - valueInLocals(componentNode.data !.locals !, localIndex)[selectorIndex]; + valueInData(componentNode.data !.data !, localIndex)[selectorIndex]; for (let i = 0; i < nodesForSelector.length; i++) { const nodeToProject = nodesForSelector[i]; @@ -1148,11 +1166,11 @@ export function bindV(values: any[]): string|NO_CHANGE { if (different = creationMode) { // make a copy of the array. if (typeof currentView.bindingStartIndex !== 'number') { - bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + bindingIndex = currentView.bindingStartIndex = data.length; } - nodesAndBindings[bindingIndex++] = parts = values.slice(); + data[bindingIndex++] = parts = values.slice(); } else { - parts = nodesAndBindings[bindingIndex++]; + parts = data[bindingIndex++]; different = false; for (let i = 0; i < values.length; i++) { different = different || values[i] !== NO_CHANGE && isDifferent(values[i], parts[i]); @@ -1181,12 +1199,12 @@ export function bind(value: T | NO_CHANGE): T|NO_CHANGE { let different: boolean; if (different = creationMode) { if (typeof currentView.bindingStartIndex !== 'number') { - bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + bindingIndex = currentView.bindingStartIndex = data.length; } - nodesAndBindings[bindingIndex++] = value; + data[bindingIndex++] = value; } else { - if (different = value !== NO_CHANGE && isDifferent(nodesAndBindings[bindingIndex], value)) { - nodesAndBindings[bindingIndex] = value; + if (different = value !== NO_CHANGE && isDifferent(data[bindingIndex], value)) { + data[bindingIndex] = value; } bindingIndex++; } @@ -1218,11 +1236,11 @@ export function bind2(prefix: string, v0: any, i0: string, v1: any, suffix: stri let different: boolean; if (different = creationMode) { if (typeof currentView.bindingStartIndex !== 'number') { - bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + bindingIndex = currentView.bindingStartIndex = data.length; } - nodesAndBindings[bindingIndex++] = {v0: v0, v1: v1}; + data[bindingIndex++] = {v0: v0, v1: v1}; } else { - const parts: {v0: any, v1: any} = nodesAndBindings[bindingIndex++]; + const parts: {v0: any, v1: any} = data[bindingIndex++]; if (v0 === NO_CHANGE) v0 = parts.v0; if (v1 === NO_CHANGE) v1 = parts.v1; if (different = (isDifferent(parts.v0, v0) || isDifferent(parts.v1, v1))) { @@ -1250,11 +1268,11 @@ export function bind3( let different: boolean; if (different = creationMode) { if (typeof currentView.bindingStartIndex !== 'number') { - bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + bindingIndex = currentView.bindingStartIndex = data.length; } - nodesAndBindings[bindingIndex++] = {v0: v0, v1: v1, v2: v2}; + data[bindingIndex++] = {v0: v0, v1: v1, v2: v2}; } else { - const parts: {v0: any, v1: any, v2: any} = nodesAndBindings[bindingIndex++]; + const parts: {v0: any, v1: any, v2: any} = data[bindingIndex++]; if (v0 === NO_CHANGE) v0 = parts.v0; if (v1 === NO_CHANGE) v1 = parts.v1; if (v2 === NO_CHANGE) v2 = parts.v2; @@ -1288,11 +1306,11 @@ export function bind4( let different: boolean; if (different = creationMode) { if (typeof currentView.bindingStartIndex !== 'number') { - bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + bindingIndex = currentView.bindingStartIndex = data.length; } - nodesAndBindings[bindingIndex++] = {v0: v0, v1: v1, v2: v2, v3: v3}; + data[bindingIndex++] = {v0: v0, v1: v1, v2: v2, v3: v3}; } else { - const parts: {v0: any, v1: any, v2: any, v3: any} = nodesAndBindings[bindingIndex++]; + const parts: {v0: any, v1: any, v2: any, v3: any} = data[bindingIndex++]; if (v0 === NO_CHANGE) v0 = parts.v0; if (v1 === NO_CHANGE) v1 = parts.v1; if (v2 === NO_CHANGE) v2 = parts.v2; @@ -1333,11 +1351,11 @@ export function bind5( let different: boolean; if (different = creationMode) { if (typeof currentView.bindingStartIndex !== 'number') { - bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + bindingIndex = currentView.bindingStartIndex = data.length; } - nodesAndBindings[bindingIndex++] = {v0: v0, v1: v1, v2: v2, v3, v4}; + data[bindingIndex++] = {v0: v0, v1: v1, v2: v2, v3, v4}; } else { - const parts: {v0: any, v1: any, v2: any, v3: any, v4: any} = nodesAndBindings[bindingIndex++]; + const parts: {v0: any, v1: any, v2: any, v3: any, v4: any} = data[bindingIndex++]; if (v0 === NO_CHANGE) v0 = parts.v0; if (v1 === NO_CHANGE) v1 = parts.v1; if (v2 === NO_CHANGE) v2 = parts.v2; @@ -1382,12 +1400,11 @@ export function bind6( let different: boolean; if (different = creationMode) { if (typeof currentView.bindingStartIndex !== 'number') { - bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + bindingIndex = currentView.bindingStartIndex = data.length; } - nodesAndBindings[bindingIndex++] = {v0: v0, v1: v1, v2: v2, v3: v3, v4: v4, v5: v5}; + data[bindingIndex++] = {v0: v0, v1: v1, v2: v2, v3: v3, v4: v4, v5: v5}; } else { - const parts: {v0: any, v1: any, v2: any, v3: any, v4: any, v5: any} = - nodesAndBindings[bindingIndex++]; + const parts: {v0: any, v1: any, v2: any, v3: any, v4: any, v5: any} = data[bindingIndex++]; if (v0 === NO_CHANGE) v0 = parts.v0; if (v1 === NO_CHANGE) v1 = parts.v1; if (v2 === NO_CHANGE) v2 = parts.v2; @@ -1437,12 +1454,12 @@ export function bind7( let different: boolean; if (different = creationMode) { if (typeof currentView.bindingStartIndex !== 'number') { - bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + bindingIndex = currentView.bindingStartIndex = data.length; } - nodesAndBindings[bindingIndex++] = {v0: v0, v1: v1, v2: v2, v3: v3, v4: v4, v5: v5, v6: v6}; + data[bindingIndex++] = {v0: v0, v1: v1, v2: v2, v3: v3, v4: v4, v5: v5, v6: v6}; } else { const parts: {v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any} = - nodesAndBindings[bindingIndex++]; + data[bindingIndex++]; if (v0 === NO_CHANGE) v0 = parts.v0; if (v1 === NO_CHANGE) v1 = parts.v1; if (v2 === NO_CHANGE) v2 = parts.v2; @@ -1497,13 +1514,12 @@ export function bind8( let different: boolean; if (different = creationMode) { if (typeof currentView.bindingStartIndex !== 'number') { - bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + bindingIndex = currentView.bindingStartIndex = data.length; } - nodesAndBindings[bindingIndex++] = - {v0: v0, v1: v1, v2: v2, v3: v3, v4: v4, v5: v5, v6: v6, v7: v7}; + data[bindingIndex++] = {v0: v0, v1: v1, v2: v2, v3: v3, v4: v4, v5: v5, v6: v6, v7: v7}; } else { const parts: {v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, v7: any} = - nodesAndBindings[bindingIndex++]; + data[bindingIndex++]; if (v0 === NO_CHANGE) v0 = parts.v0; if (v1 === NO_CHANGE) v1 = parts.v1; if (v2 === NO_CHANGE) v2 = parts.v2; @@ -1533,16 +1549,15 @@ export function bind8( } export function memory(index: number, value?: T): T { - const _locals = locals || (locals = currentView.locals = []); - return valueInLocals(_locals, index, value); + return valueInData(data, index, value); } -function valueInLocals(locals: any[], index: number, value?: T): T { - ngDevMode && assertLocalsInRange(locals, index); +function valueInData(data: any[], index: number, value?: T): T { + ngDevMode && assertDataInRange(index, data); if (value === undefined) { - value = locals[index]; + value = data[index]; } else { - locals[index] = value; + data[index] = value; } return value !; } @@ -1565,12 +1580,9 @@ function assertHasParent() { assertNotEqual(previousOrParentNode.parent, null, 'isParent'); } -function assertLocalsInRange(locals: any[], index: number) { - assertLessThan(locals ? locals.length : 0, index, 'locals.length'); -} - -function assertNodesInRange(index: number) { - assertLessThan(nodesAndBindings ? nodesAndBindings.length : 0, index, 'nodes.length'); +function assertDataInRange(index: number, arr?: any[]) { + if (arr == null) arr = data; + assertLessThan(arr ? arr.length : 0, index, 'data.length'); } function assertDirectivesInRange(index: number) { diff --git a/packages/core/src/render3/interfaces.ts b/packages/core/src/render3/interfaces.ts index 09ec73492c..a9b46fbb40 100644 --- a/packages/core/src/render3/interfaces.ts +++ b/packages/core/src/render3/interfaces.ts @@ -94,7 +94,7 @@ export interface ViewState { * do this by creating ViewState in incomplete state with nodes == null * and we initialize it on first run. */ - readonly nodesAndBindings: any[]; + readonly data: any[]; /** * All directives created inside this view. Stored as an array @@ -159,8 +159,6 @@ export interface ViewState { * in the same container. We need a way to link component views as well. */ next: ViewState|ContainerState|null; - - locals: any[]|null; } export interface LNodeInjector { @@ -291,10 +289,10 @@ export interface LNode { query: QueryState|null; /** - * Pointer to the corresponding NodeBindings object, which stores static + * Pointer to the corresponding LNodeStatic object, which stores static * data about this node. */ - nodeBindings: NodeBindings|null; + staticData: LNodeStatic|null; } /** @@ -457,7 +455,7 @@ export type InitialInputs = string[]; * - Null: that property's data was already generated and nothing was found. * - Undefined: that property's data has not yet been generated */ -export interface NodeBindings { +export interface LNodeStatic { /** The tag name associated with this node. */ tagName: string|null; @@ -483,8 +481,31 @@ export interface NodeBindings { /** Output data for all directives on this node. */ outputs: MinificationData|null|undefined; + + /** + * If this LNodeStatic corresponds to an LContainer, the container will + * need to have nested static data for each of its embedded views. + * Otherwise, nodes in embedded views with the same index as nodes + * in their parent views will overwrite each other, as they are in + * the same template. + * + * Each index in this array corresponds to the static data for a certain + * view. So if you had V(0) and V(1) in a container, you might have: + * + * [ + * [{tagName: 'div', attrs: ...}, null], // V(0) ngData + * [{tagName: 'button', attrs ...}, null] // V(1) ngData + * ] + */ + containerStatic: (LNodeStatic|null)[][]|null; } +/** Static data for an LElement */ +export interface LElementStatic extends LNodeStatic { containerStatic: null; } + +/** Static data for an LContainer */ +export interface LContainerStatic extends LNodeStatic { containerStatic: (LNodeStatic|null)[][]; } + /** Interface necessary to work with view tree traversal */ export interface ViewOrContainerState { next: ViewState|ContainerState|null; diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 1ab6af45b0..86e3d3bb20 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -37,7 +37,7 @@ export function findBeforeNode(index: number, state: ContainerState, native: RCo const children = state.children; // Find the node to insert in front of return index + 1 < children.length ? - (children[index + 1].data.nodesAndBindings[0] as LText | LElement | LContainer).native : + (children[index + 1].child as LText | LElement | LContainer).native : native; } @@ -50,7 +50,7 @@ export function addRemoveViewFromContainer( ngDevMode && assertNodeType(container, LNodeFlags.Container); ngDevMode && assertNodeType(rootNode, LNodeFlags.View); const parent = findNativeParent(container); - let node: LNode|null = rootNode.data.nodesAndBindings[0]; + let node: LNode|null = rootNode.child; if (parent) { while (node) { const type = node.flags & LNodeFlags.TYPE_MASK; @@ -78,13 +78,11 @@ export function addRemoveViewFromContainer( (isFnRenderer ? (renderer as Renderer3Fn).removeChild !(parent as RElement, node.native !) : parent.removeChild(node.native !)); - nextNode = childContainerData.children.length ? - childContainerData.children[0].data.nodesAndBindings[0] : - null; + nextNode = childContainerData.children.length ? childContainerData.children[0].child : null; } else if (type === LNodeFlags.Projection) { nextNode = (node as LProjection).data[0]; } else { - nextNode = (node as LView).data.nodesAndBindings[0]; + nextNode = (node as LView).child; } if (nextNode === null) { while (node && !node.next) { @@ -277,10 +275,12 @@ export function processProjectedNode( currentParent: LView | LElement, currentView: ViewState) { if ((node.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Container && (currentParent.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Element && - currentParent.data === null) { - // The node we are adding is a Container and we are adding it to Element - // which is not Component (no more re-projection). Assignee the final - // projection location. + (currentParent.data === null || currentParent.data === currentView)) { + // The node we are adding is a Container and we are adding it to Element which + // is not a component (no more re-projection). + // Alternatively a container is projected at the root of a component's template + // and can't be re-projected (as not content of any component). + // Assignee the final projection location in those cases. const containerState = (node as LContainer).data; containerState.renderParent = currentParent as LElement; const views = containerState.children; diff --git a/packages/core/src/render3/node_selector_matcher.ts b/packages/core/src/render3/node_selector_matcher.ts index 09bee1dccd..f3e18417c2 100644 --- a/packages/core/src/render3/node_selector_matcher.ts +++ b/packages/core/src/render3/node_selector_matcher.ts @@ -9,7 +9,7 @@ import './ng_dev_mode'; import {assertNotNull} from './assert'; -import {CSSSelector, CSSSelectorWithNegations, NodeBindings, SimpleCSSSelector} from './interfaces'; +import {CSSSelector, CSSSelectorWithNegations, LNodeStatic, SimpleCSSSelector} from './interfaces'; function isCssClassMatching(nodeClassAttrVal: string, cssClassToMatch: string): boolean { const nodeClassesLen = nodeClassAttrVal.length; @@ -28,12 +28,12 @@ function isCssClassMatching(nodeClassAttrVal: string, cssClassToMatch: string): /** * A utility function to match an Ivy node static data against a simple CSS selector * - * @param {NodeBindings} node static data to match + * @param {LNodeStatic} node static data to match * @param {SimpleCSSSelector} selector * @returns {boolean} */ export function isNodeMatchingSimpleSelector( - lNodeStaticData: NodeBindings, selector: SimpleCSSSelector): boolean { + lNodeStaticData: LNodeStatic, selector: SimpleCSSSelector): boolean { const noOfSelectorParts = selector.length; ngDevMode && assertNotNull(selector[0], 'selector[0]'); const tagNameInSelector = selector[0]; @@ -83,7 +83,7 @@ export function isNodeMatchingSimpleSelector( } export function isNodeMatchingSelectorWithNegations( - lNodeStaticData: NodeBindings, selector: CSSSelectorWithNegations): boolean { + lNodeStaticData: LNodeStatic, selector: CSSSelectorWithNegations): boolean { const positiveSelector = selector[0]; if (positiveSelector != null && !isNodeMatchingSimpleSelector(lNodeStaticData, positiveSelector)) { @@ -105,7 +105,7 @@ export function isNodeMatchingSelectorWithNegations( } export function isNodeMatchingSelector( - lNodeStaticData: NodeBindings, selector: CSSSelector): boolean { + lNodeStaticData: LNodeStatic, selector: CSSSelector): boolean { for (let i = 0; i < selector.length; i++) { if (isNodeMatchingSelectorWithNegations(lNodeStaticData, selector[i])) { return true; diff --git a/packages/core/src/render3/public_interfaces.ts b/packages/core/src/render3/public_interfaces.ts index 1397c21a5c..11ea849431 100644 --- a/packages/core/src/render3/public_interfaces.ts +++ b/packages/core/src/render3/public_interfaces.ts @@ -14,7 +14,7 @@ import {diPublic, refreshComponent} from './instructions'; * Definition of what a template rendering function should look like. */ export type ComponentTemplate = { - (ctx: T, creationMode: boolean): void; ngData?: never; + (ctx: T, creationMode: boolean): void; ngStaticData?: never; }; export type EmbeddedTemplate = (ctx: T) => void; diff --git a/packages/core/src/render3/query.ts b/packages/core/src/render3/query.ts index 30376caf64..8e9a9ff236 100644 --- a/packages/core/src/render3/query.ts +++ b/packages/core/src/render3/query.ts @@ -7,9 +7,7 @@ */ import {Observable} from 'rxjs/Observable'; - import {QueryList as IQueryList, Type} from '../core'; - import {assertNotNull} from './assert'; import {LContainer, LNode, LNodeFlags, LView, QueryState} from './interfaces'; diff --git a/packages/core/test/render3/content_spec.ts b/packages/core/test/render3/content_spec.ts index 0d925cfedc..1b81a39ddb 100644 --- a/packages/core/test/render3/content_spec.ts +++ b/packages/core/test/render3/content_spec.ts @@ -19,8 +19,8 @@ describe('content projection', () => { const Child = createComponent('child', function(ctx: any, cm: boolean) { if (cm) { m(0, dP()); - E(0, 'div'); - { P(1, 0); } + E(1, 'div'); + { P(2, 0); } e(); } }); @@ -47,7 +47,7 @@ describe('content projection', () => { const Child = createComponent('child', function(ctx: any, cm: boolean) { if (cm) { m(0, dP()); - P(0, 0); + P(1, 0); } }); const Parent = createComponent('parent', function(ctx: any, cm: boolean) { @@ -69,21 +69,21 @@ describe('content projection', () => { const GrandChild = createComponent('grand-child', function(ctx: any, cm: boolean) { if (cm) { m(0, dP()); - E(0, 'div'); - { P(1, 0); } + E(1, 'div'); + { P(2, 0); } e(); } }); const Child = createComponent('child', function(ctx: any, cm: boolean) { if (cm) { m(0, dP()); - E(0, GrandChild.ngComponentDef); + E(1, GrandChild.ngComponentDef); { D(0, GrandChild.ngComponentDef.n(), GrandChild.ngComponentDef); - P(1, 0); + P(2, 0); } e(); - GrandChild.ngComponentDef.r(0, 0); + GrandChild.ngComponentDef.r(0, 1); } }); const Parent = createComponent('parent', function(ctx: any, cm: boolean) { @@ -109,8 +109,8 @@ describe('content projection', () => { const Child = createComponent('child', function(ctx: any, cm: boolean) { if (cm) { m(0, dP()); - E(0, 'div'); - { P(1, 0); } + E(1, 'div'); + { P(2, 0); } e(); } }); @@ -148,12 +148,53 @@ describe('content projection', () => { expect(toHtml(parent)).toEqual('
()
'); }); + it('should project content with container into root', () => { + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, dP()); + P(1, 0); + } + }); + const Parent = createComponent('parent', function(ctx: {value: any}, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, Child.ngComponentDef.n(), Child.ngComponentDef); + C(1); + c(); + } + e(); + } + rC(1); + { + if (ctx.value) { + if (V(0)) { + T(0, 'content'); + } + v(); + } + } + rc(); + Child.ngComponentDef.r(0, 0); + }); + const parent = renderComponent(Parent); + expect(toHtml(parent)).toEqual(''); + + parent.value = true; + detectChanges(parent); + expect(toHtml(parent)).toEqual('content'); + + parent.value = false; + detectChanges(parent); + expect(toHtml(parent)).toEqual(''); + }); + it('should project content with container and if-else.', () => { const Child = createComponent('child', function(ctx: any, cm: boolean) { if (cm) { m(0, dP()); - E(0, 'div'); - { P(1, 0); } + E(1, 'div'); + { P(2, 0); } e(); } }); @@ -211,14 +252,14 @@ describe('content projection', () => { const Child = createComponent('child', function(ctx: any, cm: boolean) { if (cm) { m(0, dP()); - E(0, 'div'); + E(1, 'div'); { - C(1); + C(2); c(); } e(); } - rC(1); + rC(2); { if (!ctx.skipContent) { if (V(0)) { @@ -268,14 +309,14 @@ describe('content projection', () => { const Child = createComponent('child', function(ctx: any, cm: boolean) { if (cm) { m(0, dP()); - E(0, 'div'); + E(1, 'div'); { - C(1); + C(2); c(); } e(); } - rC(1); + rC(2); { if (!ctx.skipContent) { if (V(0)) { @@ -317,11 +358,11 @@ describe('content projection', () => { const Child = createComponent('child', function(ctx: any, cm: boolean) { if (cm) { m(0, dP()); - E(0, 'div'); - { P(1, 0); } + E(1, 'div'); + { P(2, 0); } e(); - E(2, 'span'); - { P(3, 0); } + E(3, 'span'); + { P(4, 0); } e(); } }); @@ -366,15 +407,15 @@ describe('content projection', () => { const Child = createComponent('child', function(ctx: any, cm: boolean) { if (cm) { m(0, dP()); - P(0, 0); - E(1, 'div'); + P(1, 0); + E(2, 'div'); { - C(2); + C(3); c(); } e(); } - rC(2); + rC(3); { if (ctx.show) { if (V(0)) { @@ -419,11 +460,11 @@ describe('content projection', () => { if (cm) { m(0, dP([[[['span', 'title', 'toFirst'], null]], [[['span', 'title', 'toSecond'], null]]])); - E(0, 'div', ['id', 'first']); - { P(1, 0, 1); } + E(1, 'div', ['id', 'first']); + { P(2, 0, 1); } e(); - E(2, 'div', ['id', 'second']); - { P(3, 0, 2); } + E(3, 'div', ['id', 'second']); + { P(4, 0, 2); } e(); } }); @@ -466,11 +507,11 @@ describe('content projection', () => { if (cm) { m(0, dP([[[['span', 'class', 'toFirst'], null]], [[['span', 'class', 'toSecond'], null]]])); - E(0, 'div', ['id', 'first']); - { P(1, 0, 1); } + E(1, 'div', ['id', 'first']); + { P(2, 0, 1); } e(); - E(2, 'div', ['id', 'second']); - { P(3, 0, 2); } + E(3, 'div', ['id', 'second']); + { P(4, 0, 2); } e(); } }); @@ -513,11 +554,11 @@ describe('content projection', () => { if (cm) { m(0, dP([[[['span', 'class', 'toFirst'], null]], [[['span', 'class', 'toSecond'], null]]])); - E(0, 'div', ['id', 'first']); - { P(1, 0, 1); } + E(1, 'div', ['id', 'first']); + { P(2, 0, 1); } e(); - E(2, 'div', ['id', 'second']); - { P(3, 0, 2); } + E(3, 'div', ['id', 'second']); + { P(4, 0, 2); } e(); } }); @@ -559,11 +600,11 @@ describe('content projection', () => { const Child = createComponent('child', function(ctx: any, cm: boolean) { if (cm) { m(0, dP([[[['span'], null]], [[['span', 'class', 'toSecond'], null]]])); - E(0, 'div', ['id', 'first']); - { P(1, 0, 1); } + E(1, 'div', ['id', 'first']); + { P(2, 0, 1); } e(); - E(2, 'div', ['id', 'second']); - { P(3, 0, 2); } + E(3, 'div', ['id', 'second']); + { P(4, 0, 2); } e(); } }); @@ -605,11 +646,11 @@ describe('content projection', () => { const Child = createComponent('child', function(ctx: any, cm: boolean) { if (cm) { m(0, dP([[[['span', 'class', 'toFirst'], null]]])); - E(0, 'div', ['id', 'first']); - { P(1, 0, 1); } + E(1, 'div', ['id', 'first']); + { P(2, 0, 1); } e(); - E(2, 'div', ['id', 'second']); - { P(3, 0); } + E(3, 'div', ['id', 'second']); + { P(4, 0); } e(); } }); @@ -652,11 +693,11 @@ describe('content projection', () => { const Child = createComponent('child', function(ctx: any, cm: boolean) { if (cm) { m(0, dP([[[['span', 'class', 'toSecond'], null]]])); - E(0, 'div', ['id', 'first']); - { P(1, 0); } + E(1, 'div', ['id', 'first']); + { P(2, 0); } e(); - E(2, 'div', ['id', 'second']); - { P(3, 0, 1); } + E(3, 'div', ['id', 'second']); + { P(4, 0, 1); } e(); } }); @@ -706,10 +747,10 @@ describe('content projection', () => { const GrandChild = createComponent('grand-child', function(ctx: any, cm: boolean) { if (cm) { m(0, dP([[[['span'], null]]])); - P(0, 0, 1); - E(1, 'hr'); + P(1, 0, 1); + E(2, 'hr'); e(); - P(2, 0, 0); + P(3, 0, 0); } }); @@ -722,16 +763,16 @@ describe('content projection', () => { const Child = createComponent('child', function(ctx: any, cm: boolean) { if (cm) { m(0, dP()); - E(0, GrandChild.ngComponentDef); + E(1, GrandChild.ngComponentDef); { D(0, GrandChild.ngComponentDef.n(), GrandChild.ngComponentDef); - P(1, 0); - E(2, 'span'); - { T(3, 'in child template'); } + P(2, 0); + E(3, 'span'); + { T(4, 'in child template'); } e(); } e(); - GrandChild.ngComponentDef.r(0, 0); + GrandChild.ngComponentDef.r(0, 1); } }); @@ -772,8 +813,8 @@ describe('content projection', () => { const Child = createComponent('child', function(ctx: any, cm: boolean) { if (cm) { m(0, dP([[[['div'], null]]])); - E(0, 'span'); - { P(1, 0, 1); } + E(1, 'span'); + { P(2, 0, 1); } e(); } }); diff --git a/packages/core/test/render3/di_spec.ts b/packages/core/test/render3/di_spec.ts index a79e82fe45..cbe028c6a9 100644 --- a/packages/core/test/render3/di_spec.ts +++ b/packages/core/test/render3/di_spec.ts @@ -10,7 +10,7 @@ import {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core'; import {bloomFindPossibleInjector} from '../../src/render3/di'; import {C, D, E, PublicFeature, T, V, b, b2, c, defineDirective, e, inject, injectElementRef, injectTemplateRef, injectViewContainerRef, rC, rc, t, v} from '../../src/render3/index'; -import {bloomAdd, createNode, createViewState, enterView, getOrCreateNodeInjector, leaveView} from '../../src/render3/instructions'; +import {bloomAdd, createLNode, createViewState, enterView, getOrCreateNodeInjector, leaveView} from '../../src/render3/instructions'; import {LNodeFlags, LNodeInjector} from '../../src/render3/interfaces'; import {renderToHtml} from './render_util'; @@ -321,7 +321,7 @@ describe('di', () => { const contentView = createViewState(-1, null !); const oldView = enterView(contentView, null !); try { - const parent = createNode(0, LNodeFlags.Element, null, null); + const parent = createLNode(0, LNodeFlags.Element, null, null); // Simulate the situation where the previous parent is not initialized. // This happens on first bootstrap because we don't init existing values diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index 5bf5d7e740..5596cd4933 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -589,4 +589,54 @@ describe('iv integration test', () => { }); }); + describe('template data', () => { + + it('should re-use template data and node data', () => { + /** + * % if (condition) { + *
+ * % } + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + C(0); + c(); + } + rC(0); + { + if (ctx.condition) { + if (V(0)) { + E(0, 'div'); + {} + e(); + } + v(); + } + } + rc(); + } + + expect((Template as any).ngStaticData).toBeUndefined(); + + renderToHtml(Template, {condition: true}); + + const oldTemplateData = (Template as any).ngStaticData; + const oldContainerData = (oldTemplateData as any)[0]; + const oldElementData = oldContainerData.containerStatic[0][0]; + expect(oldContainerData).not.toBeNull(); + expect(oldElementData).not.toBeNull(); + + renderToHtml(Template, {condition: false}); + renderToHtml(Template, {condition: true}); + + const newTemplateData = (Template as any).ngStaticData; + const newContainerData = (oldTemplateData as any)[0]; + const newElementData = oldContainerData.containerStatic[0][0]; + expect(newTemplateData === oldTemplateData).toBe(true); + expect(newContainerData === oldContainerData).toBe(true); + expect(newElementData === oldElementData).toBe(true); + }); + + }); + }); diff --git a/packages/core/test/render3/node_selector_matcher_spec.ts b/packages/core/test/render3/node_selector_matcher_spec.ts index 82a9c27372..63c04b75db 100644 --- a/packages/core/test/render3/node_selector_matcher_spec.ts +++ b/packages/core/test/render3/node_selector_matcher_spec.ts @@ -6,11 +6,18 @@ * found in the LICENSE file at https://angular.io/license */ -import {CSSSelector, CSSSelectorWithNegations, NodeBindings, SimpleCSSSelector} from '../../src/render3/interfaces'; +import {CSSSelector, CSSSelectorWithNegations, LNodeStatic, SimpleCSSSelector} from '../../src/render3/interfaces'; import {isNodeMatchingSelector, isNodeMatchingSelectorWithNegations, isNodeMatchingSimpleSelector} from '../../src/render3/node_selector_matcher'; -function testLStaticData(tagName: string, attrs: string[] | null): NodeBindings { - return {tagName, attrs, initialInputs: undefined, inputs: undefined, outputs: undefined}; +function testLStaticData(tagName: string, attrs: string[] | null): LNodeStatic { + return { + tagName, + attrs, + initialInputs: undefined, + inputs: undefined, + outputs: undefined, + containerStatic: null + }; } describe('css selector matching', () => { diff --git a/packages/core/test/render3/outputs_spec.ts b/packages/core/test/render3/outputs_spec.ts index 1a8ca3e231..d306b7fe7c 100644 --- a/packages/core/test/render3/outputs_spec.ts +++ b/packages/core/test/render3/outputs_spec.ts @@ -28,6 +28,17 @@ describe('outputs', () => { }); } + let otherDir: OtherDir; + + class OtherDir { + changeStream = new EventEmitter(); + + static ngDirectiveDef = defineDirective({ + type: OtherDir, + factory: () => otherDir = new OtherDir, + outputs: {changeStream: 'change'} + }); + } it('should call component output function when event is emitted', () => { /** */ @@ -328,15 +339,6 @@ describe('outputs', () => { }); it('should work with two outputs of the same name', () => { - let otherDir: OtherDir; - - class OtherDir { - change = new EventEmitter(); - - static ngDirectiveDef = defineDirective( - {type: OtherDir, factory: () => otherDir = new OtherDir, outputs: {change: 'change'}}); - } - /** */ function Template(ctx: any, cm: boolean) { if (cm) { @@ -357,7 +359,7 @@ describe('outputs', () => { buttonToggle !.change.next(); expect(counter).toEqual(1); - otherDir !.change.next(); + otherDir !.changeStream.next(); expect(counter).toEqual(2); }); @@ -397,4 +399,67 @@ describe('outputs', () => { expect(counter).toEqual(1); }); + it('should work with outputs at same index in if block', () => { + /** + * // outputs: null + * % if (condition) { + * // outputs: {change: [0, 'change']} + * % } else { + *
// outputs: {change: [0, + * 'changeStream']} + * % } + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'button'); + { + L('click', ctx.onClick.bind(ctx)); + T(1, 'Click me'); + } + e(); + C(2); + c(); + } + rC(2); + { + if (ctx.condition) { + if (V(0)) { + E(0, ButtonToggle.ngComponentDef); + { + D(0, ButtonToggle.ngComponentDef.n(), ButtonToggle.ngComponentDef); + L('change', ctx.onChange.bind(ctx)); + } + e(); + } + v(); + } else { + if (V(1)) { + E(0, 'div'); + { + D(0, OtherDir.ngDirectiveDef.n(), OtherDir.ngDirectiveDef); + L('change', ctx.onChange.bind(ctx)); + } + e(); + } + v(); + } + } + rc(); + } + + let counter = 0; + const ctx = {condition: true, onChange: () => counter++, onClick: () => {}}; + renderToHtml(Template, ctx); + + buttonToggle !.change.next(); + expect(counter).toEqual(1); + + ctx.condition = false; + renderToHtml(Template, ctx); + expect(counter).toEqual(1); + + otherDir !.changeStream.next(); + expect(counter).toEqual(2); + }); + }); diff --git a/packages/core/test/render3/properties_spec.ts b/packages/core/test/render3/properties_spec.ts index 6b68088306..beb87b3e5b 100644 --- a/packages/core/test/render3/properties_spec.ts +++ b/packages/core/test/render3/properties_spec.ts @@ -239,6 +239,73 @@ describe('elementProperty', () => { renderToHtml(Template, ctx); expect(otherDir !.id).toEqual(2); }); + + it('should support unrelated element properties at same index in if-else block', () => { + let idDir: IdDir; + + class IdDir { + idNumber: number; + + static ngDirectiveDef = defineDirective( + {type: IdDir, factory: () => idDir = new IdDir(), inputs: {idNumber: 'id'}}); + } + + /** + * // inputs: {'id': [0, 'idNumber']} + * % if (condition) { + * // inputs: null + * % } else { + * // inputs: {'id': [0, 'id']} + * % } + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'button'); + { + D(0, IdDir.ngDirectiveDef.n(), IdDir.ngDirectiveDef); + T(1, 'Click me'); + } + e(); + C(2); + c(); + } + p(0, 'id', b(ctx.id1)); + rC(2); + { + if (ctx.condition) { + if (V(0)) { + E(0, 'button'); + { T(1, 'Click me too'); } + e(); + } + p(0, 'id', b(ctx.id2)); + v(); + } else { + if (V(1)) { + E(0, 'button'); + { + D(0, OtherDir.ngDirectiveDef.n(), OtherDir.ngDirectiveDef); + T(1, 'Click me too'); + } + e(); + } + p(0, 'id', b(ctx.id3)); + v(); + } + } + rc(); + } + + expect(renderToHtml(Template, {condition: true, id1: 'one', id2: 'two', id3: 'three'})) + .toEqual(``); + expect(idDir !.idNumber).toEqual('one'); + + expect(renderToHtml(Template, {condition: false, id1: 'four', id2: 'two', id3: 'three'})) + .toEqual(``); + expect(idDir !.idNumber).toEqual('four'); + expect(otherDir !.id).toEqual('three'); + }); + }); describe('attributes and input properties', () => { @@ -382,6 +449,58 @@ describe('elementProperty', () => { expect(dirB !.roleB).toEqual('listbox'); }); + it('should support attributes at same index inside an if-else block', () => { + /** + *
// initialInputs: [['role', 'listbox']] + * + * % if (condition) { + *
// initialInputs: [['role', 'button']] + * % } else { + *
// initialInputs: [null] + * % } + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div', ['role', 'listbox']); + { D(0, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef); } + e(); + C(1); + c(); + } + rC(1); + { + if (ctx.condition) { + if (V(0)) { + E(0, 'div', ['role', 'button']); + { D(0, MyDirB.ngDirectiveDef.n(), MyDirB.ngDirectiveDef); } + e(); + } + v(); + } else { + if (V(1)) { + E(0, 'div', ['role', 'menu']); + {} + e(); + } + v(); + } + } + rc(); + } + + expect(renderToHtml(Template, { + condition: true + })).toEqual(`
`); + expect(myDir !.role).toEqual('listbox'); + expect(dirB !.roleB).toEqual('button'); + expect((dirB !as any).role).toBeUndefined(); + + expect(renderToHtml(Template, { + condition: false + })).toEqual(`
`); + expect(myDir !.role).toEqual('listbox'); + }); + it('should process attributes properly inside a for loop', () => { class Comp { diff --git a/packages/core/test/render3/query_spec.ts b/packages/core/test/render3/query_spec.ts index 6c2805d231..1c383e5ade 100644 --- a/packages/core/test/render3/query_spec.ts +++ b/packages/core/test/render3/query_spec.ts @@ -31,10 +31,10 @@ describe('query', () => { if (cm) { m(0, Q(Child, false)); m(1, Q(Child, true)); - E(0, Child.ngComponentDef); + E(2, Child.ngComponentDef); { child1 = D(0, Child.ngComponentDef.n(), Child.ngComponentDef); - E(1, Child.ngComponentDef); + E(3, Child.ngComponentDef); { child2 = D(1, Child.ngComponentDef.n(), Child.ngComponentDef); } e(); } diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index c96edda9fb..7ec8b79098 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -7,7 +7,7 @@ */ import {ComponentTemplate, ComponentType, PublicFeature, defineComponent, renderComponent as _renderComponent} from '../../src/render3/index'; -import {NG_HOST_SYMBOL, createNode, createViewState, renderTemplate} from '../../src/render3/instructions'; +import {NG_HOST_SYMBOL, createLNode, createViewState, renderTemplate} from '../../src/render3/instructions'; import {LElement, LNodeFlags} from '../../src/render3/interfaces'; import {RElement, RText, Renderer3} from '../../src/render3/renderer'; import {getRenderer2} from './imported_renderer2'; @@ -37,7 +37,7 @@ export function resetDOM() { requestAnimationFrame.queue = []; containerEl = document.createElement('div'); containerEl.setAttribute('host', ''); - host = createNode(null, LNodeFlags.Element, containerEl, createViewState(-1, activeRenderer)); + host = createLNode(null, LNodeFlags.Element, containerEl, createViewState(-1, activeRenderer)); // TODO: assert that the global state is clean (e.g. ngData, previousOrParentNode, etc) }