refactor(core): store locals in main array in rederer3 (#20855)

PR Close #20855
This commit is contained in:
Miško Hevery 2017-12-08 11:48:54 -08:00 committed by Igor Minar
parent 0fa818b318
commit 93b00cceb6
14 changed files with 555 additions and 242 deletions

View File

@ -10,7 +10,7 @@ import './ng_dev_mode';
import {Type} from '../core'; import {Type} from '../core';
import {assertEqual, assertLessThan, assertNotEqual, assertNotNull} from './assert'; 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 {assertNodeType} from './node_assert';
import {appendChild, insertChild, insertView, processProjectedNode, removeView} from './node_manipulation'; import {appendChild, insertChild, insertView, processProjectedNode, removeView} from './node_manipulation';
import {isNodeMatchingSelector} from './node_selector_matcher'; import {isNodeMatchingSelector} from './node_selector_matcher';
@ -45,10 +45,10 @@ let isParent: boolean;
* given type). * given type).
* *
* Each node's static data is stored at the same index that it's stored * 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. * value to avoid a sparse array.
*/ */
let ngData: (NodeBindings | null)[]; let ngStaticData: (LNodeStatic | null)[];
/** /**
* State of the current view being processed. * State of the current view being processed.
@ -63,17 +63,10 @@ let currentQuery: QueryState|null;
let creationMode: boolean; let creationMode: boolean;
/** /**
* An array of nodes (text, element, container, etc) and their bindings * An array of nodes (text, element, container, etc), their bindings, and
* in the current view * any local variables that need to be stored between invocations.
*/ */
let nodesAndBindings: any[]; let data: 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;
/** /**
* An array of directives in the current view * 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 { export function enterView(newViewState: ViewState, host: LElement | LView | null): ViewState {
const oldViewState = currentView; const oldViewState = currentView;
directives = newViewState.directives; directives = newViewState.directives;
nodesAndBindings = newViewState.nodesAndBindings; data = newViewState.data;
bindingIndex = newViewState.bindingStartIndex || 0; bindingIndex = newViewState.bindingStartIndex || 0;
if (creationMode = !nodesAndBindings) { if (creationMode = !data) {
// Absence of nodes implies creationMode. // Absence of data implies creationMode.
(newViewState as{nodesAndBindings: LNode[]}).nodesAndBindings = nodesAndBindings = []; (newViewState as{data: any[]}).data = data = [];
} }
cleanup = newViewState.cleanup; cleanup = newViewState.cleanup;
renderer = newViewState.renderer; renderer = newViewState.renderer;
locals = newViewState.locals;
if (host != null) { if (host != null) {
previousOrParentNode = host; previousOrParentNode = host;
@ -152,11 +144,10 @@ export function createViewState(viewId: number, renderer: Renderer3): ViewState
parent: currentView, parent: currentView,
id: viewId, // -1 for component views id: viewId, // -1 for component views
node: null !, // until we initialize it in createNode. node: null !, // until we initialize it in createNode.
nodesAndBindings: null !, // Hack use as a marker for creationMode data: null !, // Hack use as a marker for creationMode
directives: [], directives: [],
cleanup: null, cleanup: null,
renderer: renderer, renderer: renderer,
locals: null,
child: null, child: null,
tail: null, tail: null,
next: 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 * 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. * keep the execution code monomorphic and fast.
*/ */
export function createNode( export function createLNode(
index: number | null, type: LNodeFlags.Element, native: RElement | RText | null, index: number | null, type: LNodeFlags.Element, native: RElement | RText | null,
viewState?: ViewState | null): LElement; viewState?: ViewState | null): LElement;
export function createNode( export function createLNode(
index: null, type: LNodeFlags.View, native: null, viewState: ViewState): LView; index: null, type: LNodeFlags.View, native: null, viewState: ViewState): LView;
export function createNode( export function createLNode(
index: number, type: LNodeFlags.Container, native: RComment, index: number, type: LNodeFlags.Container, native: RComment,
containerState: ContainerState): LContainer; containerState: ContainerState): LContainer;
export function createNode( export function createLNode(
index: number, type: LNodeFlags.Projection, native: null, index: number, type: LNodeFlags.Projection, native: null,
projectionState: ProjectionState): LProjection; projectionState: ProjectionState): LProjection;
export function createNode( export function createLNode(
index: number | null, type: LNodeFlags, native: RText | RElement | RComment | null, index: number | null, type: LNodeFlags, native: RText | RElement | RComment | null,
state?: null | ViewState | ContainerState | ProjectionState): LElement&LText&LView&LContainer& state?: null | ViewState | ContainerState | ProjectionState): LElement&LText&LView&LContainer&
LProjection { LProjection {
@ -200,7 +191,7 @@ export function createNode(
nodeInjector: parent ? parent.nodeInjector : null, nodeInjector: parent ? parent.nodeInjector : null,
data: isState ? state as any : null, data: isState ? state as any : null,
query: query, query: query,
nodeBindings: null staticData: null
}; };
if ((type & LNodeFlags.ViewOrElement) === LNodeFlags.ViewOrElement && isState) { if ((type & LNodeFlags.ViewOrElement) === LNodeFlags.ViewOrElement && isState) {
@ -211,21 +202,21 @@ export function createNode(
} }
if (index != null) { if (index != null) {
// We are Element or Container // We are Element or Container
ngDevMode && ngDevMode && assertEqual(data.length, index, 'data.length not in sequence');
assertEqual(nodesAndBindings.length, index, 'nodesAndBindings.length not in sequence'); data[index] = node;
nodesAndBindings[index] = node;
// Every node adds a value to the data array to avoid a sparse array // Every node adds a value to the static data array to avoid a sparse array
if (ngData && index >= ngData.length) { if (ngStaticData && index >= ngStaticData.length) {
ngData[index] = null; ngStaticData[index] = null;
} else if (ngData) { } else if (ngStaticData) {
node.nodeBindings = ngData[index]; node.staticData = ngStaticData[index];
} }
// Now link ourselves into the tree. // Now link ourselves into the tree.
if (isParent) { if (isParent) {
currentQuery = null; 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. // We are in the same view, which means we are adding content node to the parent View.
ngDevMode && assertEqual(previousOrParentNode.child, null, 'previousNode.child'); ngDevMode && assertEqual(previousOrParentNode.child, null, 'previousNode.child');
previousOrParentNode.child = node; previousOrParentNode.child = node;
@ -259,7 +250,7 @@ export function renderTemplate<T>(host: LElement, template: ComponentTemplate<T>
ngDevMode && assertNotEqual(hostView, null, 'hostView'); ngDevMode && assertNotEqual(hostView, null, 'hostView');
const oldView = enterView(hostView, host); const oldView = enterView(hostView, host);
try { try {
ngData = template.ngData || (template.ngData = [] as never); ngStaticData = template.ngStaticData || (template.ngStaticData = [] as never);
template(context, creationMode); template(context, creationMode);
} finally { } finally {
leaveView(oldView); leaveView(oldView);
@ -327,7 +318,7 @@ export function getOrCreateNodeInjector(): LNodeInjector {
/** /**
* Create DOM element. The instruction must later be followed by `elementEnd()` call. * 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 nameOrComponentDef Name of the DOM Node or `ComponentDef`.
* @param attrs Statically bound set of attributes to be written into the DOM element on creation. * @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) { if (nameOrComponentDef == null) {
// native node retrieval - used for exporting elements as tpl local variables (<div #foo>) // native node retrieval - used for exporting elements as tpl local variables (<div #foo>)
const node = nodesAndBindings[index] !; const node = data[index] !;
native = node && (node as LElement).native; native = node && (node as LElement).native;
} else { } else {
ngDevMode && assertEqual(currentView.bindingStartIndex, null, 'bindingStartIndex'); ngDevMode && assertEqual(currentView.bindingStartIndex, null, 'bindingStartIndex');
@ -356,12 +347,12 @@ export function elementCreate(
native = renderer.createElement(name); native = renderer.createElement(name);
// Only component views should be added to the view tree directly. Embedded views are // 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. // accessed through their containers because they may be removed / re-added later.
node = createNode( node = createLNode(
index, LNodeFlags.Element, native, index, LNodeFlags.Element, native,
isHostElement ? addToViewTree(createViewState(-1, renderer)) : null); isHostElement ? addToViewTree(createViewState(-1, renderer)) : null);
if (node.nodeBindings == null) { if (node.staticData == null) {
node.nodeBindings = ngData[index] = createNodeBindings(name, attrs || null); node.staticData = ngStaticData[index] = createStaticData(name, attrs || null, null);
} }
if (attrs) setUpAttributes(native, attrs); 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. * @param elementOrSelector Render element or CSS selector to locate the element.
*/ */
export function elementHost(elementOrSelector: RElement | string) { export function elementHost(elementOrSelector: RElement | string) {
ngDevMode && assertNodesInRange(-1); ngDevMode && assertDataInRange(-1);
const rNode = typeof elementOrSelector === 'string' ? const rNode = typeof elementOrSelector === 'string' ?
((renderer as Renderer3Fn).selectRootElement ? ((renderer as Renderer3Fn).selectRootElement ?
(renderer as Renderer3Fn).selectRootElement(elementOrSelector) : (renderer as Renderer3Fn).selectRootElement(elementOrSelector) :
@ -404,7 +395,7 @@ export function elementHost(elementOrSelector: RElement | string) {
throw createError('Host node is required:', elementOrSelector); 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); (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 (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 // checked
mergeData.outputs = null; mergeData.outputs = null;
mergeData = generateMinifiedData(node.flags, mergeData); 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. * 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 * @param attrName Name of attribute. Because it is going to DOM, this is not subject to
* renaming as port of minification. * renaming as port of minification.
* @param value Value to write. This value will go through stringification. * @param value Value to write. This value will go through stringification.
*/ */
export function elementAttribute(index: number, attrName: string, value: any): void { export function elementAttribute(index: number, attrName: string, value: any): void {
if (value !== NO_CHANGE) { if (value !== NO_CHANGE) {
const lElement = nodesAndBindings[index] as LElement; const lElement = data[index] as LElement;
if (value == null) { if (value == null) {
(renderer as Renderer3Fn).removeAttribute ? (renderer as Renderer3Fn).removeAttribute ?
(renderer as Renderer3Fn).removeAttribute(lElement.native, attrName) : (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 * be conducted at runtime as well so child components that add new @Inputs don't have to be
* re-compiled. * 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 * @param propName Name of property. Because it is going to DOM, this is not subject to
* renaming as part of minification. * renaming as part of minification.
* @param value New value to write. * @param value New value to write.
@ -517,18 +508,19 @@ export function elementAttribute(index: number, attrName: string, value: any): v
export function elementProperty<T>(index: number, propName: string, value: T | NO_CHANGE): void { export function elementProperty<T>(index: number, propName: string, value: T | NO_CHANGE): void {
if (value === NO_CHANGE) return; if (value === NO_CHANGE) return;
const node = nodesAndBindings[index] as LElement; const node = data[index] as LElement;
let data: NodeBindings|null = node.nodeBindings !; let staticData: LNodeStatic|null = node.staticData !;
// if data.inputs is undefined, a listener has created output data, but inputs haven't yet been // if staticData.inputs is undefined, a listener has created output staticData, but inputs haven't
// yet been
// checked // checked
if (data.inputs === undefined) { if (staticData.inputs === undefined) {
// mark inputs as checked // mark inputs as checked
data.inputs = null; staticData.inputs = null;
data = generateMinifiedData(node.flags, data, true); staticData = generateMinifiedData(node.flags, staticData, true);
} }
const inputData = data.inputs; const inputData = staticData.inputs;
let dataValue: MinificationDataValue|null; let dataValue: MinificationDataValue|null;
if (inputData && (dataValue = inputData[propName])) { if (inputData && (dataValue = inputData[propName])) {
setInputsForProperty(dataValue, value); setInputsForProperty(dataValue, value);
@ -541,8 +533,16 @@ export function elementProperty<T>(index: number, propName: string, value: T | N
} }
} }
function createNodeBindings(tagName: string, attrs: string[] | null): NodeBindings { function createStaticData(
return {tagName, attrs, initialInputs: undefined, inputs: undefined, outputs: undefined}; 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 * 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. * 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( function generateMinifiedData(flags: number, data: LNodeStatic, isInputData = false): LNodeStatic {
flags: number, data: NodeBindings, isInputData = false): NodeBindings {
const start = flags >> LNodeFlags.INDX_SHIFT; const start = flags >> LNodeFlags.INDX_SHIFT;
const size = (flags & LNodeFlags.SIZE_MASK) >> LNodeFlags.SIZE_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 * 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 * @param className Name of class to toggle. Because it is going to DOM, this is not subject to
* renaming as part of minification. * renaming as part of minification.
* @param value A value indicating if a given class should be added or removed. * @param value A value indicating if a given class should be added or removed.
*/ */
export function elementClass<T>(index: number, className: string, value: T | NO_CHANGE): void { export function elementClass<T>(index: number, className: string, value: T | NO_CHANGE): void {
if (value !== NO_CHANGE) { if (value !== NO_CHANGE) {
const lElement = nodesAndBindings[index] as LElement; const lElement = data[index] as LElement;
if (value) { if (value) {
(renderer as Renderer3Fn).addClass ? (renderer as Renderer3Fn).addClass ?
(renderer as Renderer3Fn).addClass(lElement.native, className) : (renderer as Renderer3Fn).addClass(lElement.native, className) :
@ -615,7 +614,7 @@ export function elementClass<T>(index: number, className: string, value: T | NO_
/** /**
* Update a given style on an Element. * 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 * @param styleName Name of property. Because it is going to DOM this is not subject to
* renaming as part of minification. * renaming as part of minification.
* @param value New value to write (null to remove). * @param value New value to write (null to remove).
@ -624,7 +623,7 @@ export function elementClass<T>(index: number, className: string, value: T | NO_
export function elementStyle<T>( export function elementStyle<T>(
index: number, styleName: string, value: T | NO_CHANGE, suffix?: string): void { index: number, styleName: string, value: T | NO_CHANGE, suffix?: string): void {
if (value !== NO_CHANGE) { if (value !== NO_CHANGE) {
const lElement = nodesAndBindings[index] as LElement; const lElement = data[index] as LElement;
if (value == null) { if (value == null) {
(renderer as Renderer3Fn).removeStyle ? (renderer as Renderer3Fn).removeStyle ?
(renderer as Renderer3Fn) (renderer as Renderer3Fn)
@ -651,7 +650,7 @@ export function elementStyle<T>(
/** /**
* Create static text node * 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. * @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. * 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 Renderer3Fn).createText(stringify(value)) :
(renderer as Renderer3oo).createTextNode !(stringify(value))) : (renderer as Renderer3oo).createTextNode !(stringify(value))) :
null; null;
const node = createNode(index, LNodeFlags.Element, textNode); const node = createLNode(index, LNodeFlags.Element, textNode);
// Text nodes are self closing. // Text nodes are self closing.
isParent = false; isParent = false;
appendChild(node.parent !, textNode, currentView); appendChild(node.parent !, textNode, currentView);
@ -672,12 +671,12 @@ export function textCreate(index: number, value?: any): void {
* Create text node with binding * Create text node with binding
* Bindings should be handled externally with the proper bind(1-8) method * 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. * @param value Stringified value to write.
*/ */
export function textCreateBound<T>(index: number, value: T | NO_CHANGE): void { export function textCreateBound<T>(index: number, value: T | NO_CHANGE): void {
// TODO(misko): I don't think index < nodes.length check is needed here. // 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 (existingNode && existingNode.native) {
// If DOM node exists and value changed, update textContent // If DOM node exists and value changed, update textContent
value !== NO_CHANGE && value !== NO_CHANGE &&
@ -744,7 +743,7 @@ export function directiveCreate<T>(
if (diPublic) { if (diPublic) {
diPublic(directiveDef !); diPublic(directiveDef !);
} }
const nodeBindings: NodeBindings|null = ngData && ngData[nodesAndBindings.length - 1]; const nodeBindings: LNodeStatic|null = previousOrParentNode.staticData;
if (nodeBindings && nodeBindings.attrs) if (nodeBindings && nodeBindings.attrs)
setInputsFromAttrs<T>(instance, directiveDef !.inputs, nodeBindings); setInputsFromAttrs<T>(instance, directiveDef !.inputs, nodeBindings);
} }
@ -756,16 +755,16 @@ export function directiveCreate<T>(
* *
* @param instance Instance of the directive on which to set the initial inputs * @param instance Instance of the directive on which to set the initial inputs
* @param inputs The list of inputs from the directive def * @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<T>( function setInputsFromAttrs<T>(
instance: T, inputs: {[key: string]: string}, nodeBindings: NodeBindings): void { instance: T, inputs: {[key: string]: string}, staticData: LNodeStatic): void {
const directiveIndex = const directiveIndex =
((previousOrParentNode.flags & LNodeFlags.SIZE_MASK) >> LNodeFlags.SIZE_SHIFT) - 1; ((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) { if (initialInputData === undefined || directiveIndex >= initialInputData.length) {
initialInputData = generateInitialInputs(directiveIndex, inputs, nodeBindings); initialInputData = generateInitialInputs(directiveIndex, inputs, staticData);
} }
const initialInputs: InitialInputs|null = initialInputData[directiveIndex]; const initialInputs: InitialInputs|null = initialInputData[directiveIndex];
@ -785,12 +784,12 @@ function setInputsFromAttrs<T>(
*/ */
function generateInitialInputs( function generateInitialInputs(
directiveIndex: number, inputs: {[key: string]: string}, directiveIndex: number, inputs: {[key: string]: string},
nodeBindings: NodeBindings): InitialInputData { staticData: LNodeStatic): InitialInputData {
const initialInputData: InitialInputData = const initialInputData: InitialInputData =
nodeBindings.initialInputs || (nodeBindings.initialInputs = []); staticData.initialInputs || (staticData.initialInputs = []);
initialInputData[directiveIndex] = null; initialInputData[directiveIndex] = null;
const attrs = nodeBindings.attrs !; const attrs = staticData.attrs !;
for (let i = 0; i < attrs.length; i += 2) { for (let i = 0; i < attrs.length; i += 2) {
const attrName = attrs[i]; const attrName = attrs[i];
const minifiedInputName = inputs[attrName]; const minifiedInputName = inputs[attrName];
@ -830,7 +829,7 @@ export function directiveLifeCycle(
* *
* Only `LView`s can go into `LContainer`. * 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 * @param template Optional inline template
*/ */
export function containerCreate( export function containerCreate(
@ -851,7 +850,7 @@ export function containerCreate(
renderParent = currentParent as LElement; renderParent = currentParent as LElement;
} }
const node = createNode(index, LNodeFlags.Container, comment, <ContainerState>{ const node = createLNode(index, LNodeFlags.Container, comment, <ContainerState>{
children: [], children: [],
nextIndex: 0, renderParent, nextIndex: 0, renderParent,
template: template == null ? null : template, template: template == null ? null : template,
@ -859,8 +858,8 @@ export function containerCreate(
parent: currentView parent: currentView
}); });
if (tagName && node.nodeBindings == null) { if (node.staticData == null) {
node.nodeBindings = ngData[index] = createNodeBindings(tagName, attrs || null); node.staticData = ngStaticData[index] = createStaticData(tagName || null, attrs || null, []);
} }
// Containers are added to the current view tree instead of their embedded views // 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. * 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 { export function refreshContainer(index: number): void {
ngDevMode && assertNodesInRange(index); ngDevMode && assertDataInRange(index);
previousOrParentNode = nodesAndBindings[index] as LNode; previousOrParentNode = data[index] as LNode;
ngDevMode && assertNodeType(previousOrParentNode, LNodeFlags.Container); ngDevMode && assertNodeType(previousOrParentNode, LNodeFlags.Container);
isParent = true; isParent = true;
(previousOrParentNode as LContainer).data.nextIndex = 0; (previousOrParentNode as LContainer).data.nextIndex = 0;
@ -938,12 +937,32 @@ export function viewCreate(viewBlockId: number): boolean {
} else { } else {
// When we create a new View, we always reset the state of the instructions. // When we create a new View, we always reset the state of the instructions.
const newViewState = createViewState(viewBlockId, renderer); const newViewState = createViewState(viewBlockId, renderer);
enterView(newViewState, createNode(null, LNodeFlags.View, null, newViewState)); enterView(newViewState, createLNode(null, LNodeFlags.View, null, newViewState));
containerState.nextIndex++; containerState.nextIndex++;
} }
setNgStaticDataForView(viewBlockId);
return !viewUpdateMode; 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. * Marks the end of the LView.
*/ */
@ -975,8 +994,8 @@ export const refreshComponent:
void = function<T>( void = function<T>(
this: undefined | {template: ComponentTemplate<T>}, directiveIndex: number, this: undefined | {template: ComponentTemplate<T>}, directiveIndex: number,
elementIndex: number, template: ComponentTemplate<T>) { elementIndex: number, template: ComponentTemplate<T>) {
ngDevMode && assertNodesInRange(elementIndex); ngDevMode && assertDataInRange(elementIndex);
const element = nodesAndBindings ![elementIndex] as LElement; const element = data ![elementIndex] as LElement;
ngDevMode && assertNodeType(element, LNodeFlags.Element); ngDevMode && assertNodeType(element, LNodeFlags.Element);
ngDevMode && assertNotEqual(element.data, null, 'isComponent'); ngDevMode && assertNotEqual(element.data, null, 'isComponent');
ngDevMode && assertDirectivesInRange(directiveIndex << 1); ngDevMode && assertDirectivesInRange(directiveIndex << 1);
@ -984,13 +1003,13 @@ export const refreshComponent:
ngDevMode && assertNotEqual(hostView, null, 'hostView'); ngDevMode && assertNotEqual(hostView, null, 'hostView');
const directive = directives[directiveIndex << 1]; const directive = directives[directiveIndex << 1];
const oldView = enterView(hostView, element); const oldView = enterView(hostView, element);
const oldNgData = ngData; const oldNgStaticData = ngStaticData;
try { try {
const _template = template || this !.template; const _template = template || this !.template;
ngData = _template.ngData || (_template.ngData = [] as never); ngStaticData = _template.ngStaticData || (_template.ngStaticData = [] as never);
_template(directive, creationMode); _template(directive, creationMode);
} finally { } finally {
ngData = oldNgData; ngStaticData = oldNgStaticData;
leaveView(oldView); leaveView(oldView);
} }
}; };
@ -1023,9 +1042,9 @@ export function distributeProjectedNodes(selectors?: CSSSelector[]): LNode[][] {
// - elements, excluding text nodes; // - elements, excluding text nodes;
// - containers that have tagName and attributes associated. // - containers that have tagName and attributes associated.
if (componentChild.nodeBindings) { if (componentChild.staticData) {
for (let i = 0; i < selectors !.length; i++) { 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); distributedNodes[i + 1].push(componentChild);
break; // first matching selector "captures" a given node break; // first matching selector "captures" a given node
} else { } else {
@ -1058,7 +1077,7 @@ export function distributeProjectedNodes(selectors?: CSSSelector[]): LNode[][] {
export function contentProjection( export function contentProjection(
nodeIndex: number, localIndex: number, selectorIndex: number = 0): void { nodeIndex: number, localIndex: number, selectorIndex: number = 0): void {
const projectedNodes: ProjectionState = []; const projectedNodes: ProjectionState = [];
const node = createNode(nodeIndex, LNodeFlags.Projection, null, projectedNodes); const node = createLNode(nodeIndex, LNodeFlags.Projection, null, projectedNodes);
isParent = false; // self closing isParent = false; // self closing
const currentParent = node.parent; const currentParent = node.parent;
@ -1066,9 +1085,8 @@ export function contentProjection(
const componentNode = findComponentHost(currentView); const componentNode = findComponentHost(currentView);
// make sure that nodes to project were memorized // make sure that nodes to project were memorized
ngDevMode && assertNotNull(componentNode.data !.locals, 'componentNode.data.locals');
const nodesForSelector = const nodesForSelector =
valueInLocals<LNode[][]>(componentNode.data !.locals !, localIndex)[selectorIndex]; valueInData<LNode[][]>(componentNode.data !.data !, localIndex)[selectorIndex];
for (let i = 0; i < nodesForSelector.length; i++) { for (let i = 0; i < nodesForSelector.length; i++) {
const nodeToProject = nodesForSelector[i]; const nodeToProject = nodesForSelector[i];
@ -1148,11 +1166,11 @@ export function bindV(values: any[]): string|NO_CHANGE {
if (different = creationMode) { if (different = creationMode) {
// make a copy of the array. // make a copy of the array.
if (typeof currentView.bindingStartIndex !== 'number') { 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 { } else {
parts = nodesAndBindings[bindingIndex++]; parts = data[bindingIndex++];
different = false; different = false;
for (let i = 0; i < values.length; i++) { for (let i = 0; i < values.length; i++) {
different = different || values[i] !== NO_CHANGE && isDifferent(values[i], parts[i]); different = different || values[i] !== NO_CHANGE && isDifferent(values[i], parts[i]);
@ -1181,12 +1199,12 @@ export function bind<T>(value: T | NO_CHANGE): T|NO_CHANGE {
let different: boolean; let different: boolean;
if (different = creationMode) { if (different = creationMode) {
if (typeof currentView.bindingStartIndex !== 'number') { if (typeof currentView.bindingStartIndex !== 'number') {
bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; bindingIndex = currentView.bindingStartIndex = data.length;
} }
nodesAndBindings[bindingIndex++] = value; data[bindingIndex++] = value;
} else { } else {
if (different = value !== NO_CHANGE && isDifferent(nodesAndBindings[bindingIndex], value)) { if (different = value !== NO_CHANGE && isDifferent(data[bindingIndex], value)) {
nodesAndBindings[bindingIndex] = value; data[bindingIndex] = value;
} }
bindingIndex++; bindingIndex++;
} }
@ -1218,11 +1236,11 @@ export function bind2(prefix: string, v0: any, i0: string, v1: any, suffix: stri
let different: boolean; let different: boolean;
if (different = creationMode) { if (different = creationMode) {
if (typeof currentView.bindingStartIndex !== 'number') { 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 { } 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 (v0 === NO_CHANGE) v0 = parts.v0;
if (v1 === NO_CHANGE) v1 = parts.v1; if (v1 === NO_CHANGE) v1 = parts.v1;
if (different = (isDifferent(parts.v0, v0) || isDifferent(parts.v1, v1))) { if (different = (isDifferent(parts.v0, v0) || isDifferent(parts.v1, v1))) {
@ -1250,11 +1268,11 @@ export function bind3(
let different: boolean; let different: boolean;
if (different = creationMode) { if (different = creationMode) {
if (typeof currentView.bindingStartIndex !== 'number') { 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 { } 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 (v0 === NO_CHANGE) v0 = parts.v0;
if (v1 === NO_CHANGE) v1 = parts.v1; if (v1 === NO_CHANGE) v1 = parts.v1;
if (v2 === NO_CHANGE) v2 = parts.v2; if (v2 === NO_CHANGE) v2 = parts.v2;
@ -1288,11 +1306,11 @@ export function bind4(
let different: boolean; let different: boolean;
if (different = creationMode) { if (different = creationMode) {
if (typeof currentView.bindingStartIndex !== 'number') { 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 { } 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 (v0 === NO_CHANGE) v0 = parts.v0;
if (v1 === NO_CHANGE) v1 = parts.v1; if (v1 === NO_CHANGE) v1 = parts.v1;
if (v2 === NO_CHANGE) v2 = parts.v2; if (v2 === NO_CHANGE) v2 = parts.v2;
@ -1333,11 +1351,11 @@ export function bind5(
let different: boolean; let different: boolean;
if (different = creationMode) { if (different = creationMode) {
if (typeof currentView.bindingStartIndex !== 'number') { 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 { } 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 (v0 === NO_CHANGE) v0 = parts.v0;
if (v1 === NO_CHANGE) v1 = parts.v1; if (v1 === NO_CHANGE) v1 = parts.v1;
if (v2 === NO_CHANGE) v2 = parts.v2; if (v2 === NO_CHANGE) v2 = parts.v2;
@ -1382,12 +1400,11 @@ export function bind6(
let different: boolean; let different: boolean;
if (different = creationMode) { if (different = creationMode) {
if (typeof currentView.bindingStartIndex !== 'number') { 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 { } else {
const parts: {v0: any, v1: any, v2: any, v3: any, v4: any, v5: any} = const parts: {v0: any, v1: any, v2: any, v3: any, v4: any, v5: any} = data[bindingIndex++];
nodesAndBindings[bindingIndex++];
if (v0 === NO_CHANGE) v0 = parts.v0; if (v0 === NO_CHANGE) v0 = parts.v0;
if (v1 === NO_CHANGE) v1 = parts.v1; if (v1 === NO_CHANGE) v1 = parts.v1;
if (v2 === NO_CHANGE) v2 = parts.v2; if (v2 === NO_CHANGE) v2 = parts.v2;
@ -1437,12 +1454,12 @@ export function bind7(
let different: boolean; let different: boolean;
if (different = creationMode) { if (different = creationMode) {
if (typeof currentView.bindingStartIndex !== 'number') { 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 { } else {
const parts: {v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any} = 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 (v0 === NO_CHANGE) v0 = parts.v0;
if (v1 === NO_CHANGE) v1 = parts.v1; if (v1 === NO_CHANGE) v1 = parts.v1;
if (v2 === NO_CHANGE) v2 = parts.v2; if (v2 === NO_CHANGE) v2 = parts.v2;
@ -1497,13 +1514,12 @@ export function bind8(
let different: boolean; let different: boolean;
if (different = creationMode) { if (different = creationMode) {
if (typeof currentView.bindingStartIndex !== 'number') { if (typeof currentView.bindingStartIndex !== 'number') {
bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; bindingIndex = currentView.bindingStartIndex = data.length;
} }
nodesAndBindings[bindingIndex++] = data[bindingIndex++] = {v0: v0, v1: v1, v2: v2, v3: v3, v4: v4, v5: v5, v6: v6, v7: v7};
{v0: v0, v1: v1, v2: v2, v3: v3, v4: v4, v5: v5, v6: v6, v7: v7};
} else { } else {
const parts: {v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, v7: any} = 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 (v0 === NO_CHANGE) v0 = parts.v0;
if (v1 === NO_CHANGE) v1 = parts.v1; if (v1 === NO_CHANGE) v1 = parts.v1;
if (v2 === NO_CHANGE) v2 = parts.v2; if (v2 === NO_CHANGE) v2 = parts.v2;
@ -1533,16 +1549,15 @@ export function bind8(
} }
export function memory<T>(index: number, value?: T): T { export function memory<T>(index: number, value?: T): T {
const _locals = locals || (locals = currentView.locals = []); return valueInData<T>(data, index, value);
return valueInLocals<T>(_locals, index, value);
} }
function valueInLocals<T>(locals: any[], index: number, value?: T): T { function valueInData<T>(data: any[], index: number, value?: T): T {
ngDevMode && assertLocalsInRange(locals, index); ngDevMode && assertDataInRange(index, data);
if (value === undefined) { if (value === undefined) {
value = locals[index]; value = data[index];
} else { } else {
locals[index] = value; data[index] = value;
} }
return value !; return value !;
} }
@ -1565,12 +1580,9 @@ function assertHasParent() {
assertNotEqual(previousOrParentNode.parent, null, 'isParent'); assertNotEqual(previousOrParentNode.parent, null, 'isParent');
} }
function assertLocalsInRange(locals: any[], index: number) { function assertDataInRange(index: number, arr?: any[]) {
assertLessThan(locals ? locals.length : 0, index, 'locals.length'); if (arr == null) arr = data;
} assertLessThan(arr ? arr.length : 0, index, 'data.length');
function assertNodesInRange(index: number) {
assertLessThan(nodesAndBindings ? nodesAndBindings.length : 0, index, 'nodes.length');
} }
function assertDirectivesInRange(index: number) { function assertDirectivesInRange(index: number) {

View File

@ -94,7 +94,7 @@ export interface ViewState {
* do this by creating ViewState in incomplete state with nodes == null * do this by creating ViewState in incomplete state with nodes == null
* and we initialize it on first run. * and we initialize it on first run.
*/ */
readonly nodesAndBindings: any[]; readonly data: any[];
/** /**
* All directives created inside this view. Stored as an array * 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. * in the same container. We need a way to link component views as well.
*/ */
next: ViewState|ContainerState|null; next: ViewState|ContainerState|null;
locals: any[]|null;
} }
export interface LNodeInjector { export interface LNodeInjector {
@ -291,10 +289,10 @@ export interface LNode {
query: QueryState|null; 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. * 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. * - Null: that property's data was already generated and nothing was found.
* - Undefined: that property's data has not yet been generated * - Undefined: that property's data has not yet been generated
*/ */
export interface NodeBindings { export interface LNodeStatic {
/** The tag name associated with this node. */ /** The tag name associated with this node. */
tagName: string|null; tagName: string|null;
@ -483,8 +481,31 @@ export interface NodeBindings {
/** Output data for all directives on this node. */ /** Output data for all directives on this node. */
outputs: MinificationData|null|undefined; 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 */ /** Interface necessary to work with view tree traversal */
export interface ViewOrContainerState { export interface ViewOrContainerState {
next: ViewState|ContainerState|null; next: ViewState|ContainerState|null;

View File

@ -37,7 +37,7 @@ export function findBeforeNode(index: number, state: ContainerState, native: RCo
const children = state.children; const children = state.children;
// Find the node to insert in front of // Find the node to insert in front of
return index + 1 < children.length ? 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; native;
} }
@ -50,7 +50,7 @@ export function addRemoveViewFromContainer(
ngDevMode && assertNodeType(container, LNodeFlags.Container); ngDevMode && assertNodeType(container, LNodeFlags.Container);
ngDevMode && assertNodeType(rootNode, LNodeFlags.View); ngDevMode && assertNodeType(rootNode, LNodeFlags.View);
const parent = findNativeParent(container); const parent = findNativeParent(container);
let node: LNode|null = rootNode.data.nodesAndBindings[0]; let node: LNode|null = rootNode.child;
if (parent) { if (parent) {
while (node) { while (node) {
const type = node.flags & LNodeFlags.TYPE_MASK; const type = node.flags & LNodeFlags.TYPE_MASK;
@ -78,13 +78,11 @@ export function addRemoveViewFromContainer(
(isFnRenderer ? (isFnRenderer ?
(renderer as Renderer3Fn).removeChild !(parent as RElement, node.native !) : (renderer as Renderer3Fn).removeChild !(parent as RElement, node.native !) :
parent.removeChild(node.native !)); parent.removeChild(node.native !));
nextNode = childContainerData.children.length ? nextNode = childContainerData.children.length ? childContainerData.children[0].child : null;
childContainerData.children[0].data.nodesAndBindings[0] :
null;
} else if (type === LNodeFlags.Projection) { } else if (type === LNodeFlags.Projection) {
nextNode = (node as LProjection).data[0]; nextNode = (node as LProjection).data[0];
} else { } else {
nextNode = (node as LView).data.nodesAndBindings[0]; nextNode = (node as LView).child;
} }
if (nextNode === null) { if (nextNode === null) {
while (node && !node.next) { while (node && !node.next) {
@ -277,10 +275,12 @@ export function processProjectedNode(
currentParent: LView | LElement, currentView: ViewState) { currentParent: LView | LElement, currentView: ViewState) {
if ((node.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Container && if ((node.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Container &&
(currentParent.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Element && (currentParent.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Element &&
currentParent.data === null) { (currentParent.data === null || currentParent.data === currentView)) {
// The node we are adding is a Container and we are adding it to Element // The node we are adding is a Container and we are adding it to Element which
// which is not Component (no more re-projection). Assignee the final // is not a component (no more re-projection).
// projection location. // 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; const containerState = (node as LContainer).data;
containerState.renderParent = currentParent as LElement; containerState.renderParent = currentParent as LElement;
const views = containerState.children; const views = containerState.children;

View File

@ -9,7 +9,7 @@
import './ng_dev_mode'; import './ng_dev_mode';
import {assertNotNull} from './assert'; 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 { function isCssClassMatching(nodeClassAttrVal: string, cssClassToMatch: string): boolean {
const nodeClassesLen = nodeClassAttrVal.length; 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 * 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 * @param {SimpleCSSSelector} selector
* @returns {boolean} * @returns {boolean}
*/ */
export function isNodeMatchingSimpleSelector( export function isNodeMatchingSimpleSelector(
lNodeStaticData: NodeBindings, selector: SimpleCSSSelector): boolean { lNodeStaticData: LNodeStatic, selector: SimpleCSSSelector): boolean {
const noOfSelectorParts = selector.length; const noOfSelectorParts = selector.length;
ngDevMode && assertNotNull(selector[0], 'selector[0]'); ngDevMode && assertNotNull(selector[0], 'selector[0]');
const tagNameInSelector = selector[0]; const tagNameInSelector = selector[0];
@ -83,7 +83,7 @@ export function isNodeMatchingSimpleSelector(
} }
export function isNodeMatchingSelectorWithNegations( export function isNodeMatchingSelectorWithNegations(
lNodeStaticData: NodeBindings, selector: CSSSelectorWithNegations): boolean { lNodeStaticData: LNodeStatic, selector: CSSSelectorWithNegations): boolean {
const positiveSelector = selector[0]; const positiveSelector = selector[0];
if (positiveSelector != null && if (positiveSelector != null &&
!isNodeMatchingSimpleSelector(lNodeStaticData, positiveSelector)) { !isNodeMatchingSimpleSelector(lNodeStaticData, positiveSelector)) {
@ -105,7 +105,7 @@ export function isNodeMatchingSelectorWithNegations(
} }
export function isNodeMatchingSelector( export function isNodeMatchingSelector(
lNodeStaticData: NodeBindings, selector: CSSSelector): boolean { lNodeStaticData: LNodeStatic, selector: CSSSelector): boolean {
for (let i = 0; i < selector.length; i++) { for (let i = 0; i < selector.length; i++) {
if (isNodeMatchingSelectorWithNegations(lNodeStaticData, selector[i])) { if (isNodeMatchingSelectorWithNegations(lNodeStaticData, selector[i])) {
return true; return true;

View File

@ -14,7 +14,7 @@ import {diPublic, refreshComponent} from './instructions';
* Definition of what a template rendering function should look like. * Definition of what a template rendering function should look like.
*/ */
export type ComponentTemplate<T> = { export type ComponentTemplate<T> = {
(ctx: T, creationMode: boolean): void; ngData?: never; (ctx: T, creationMode: boolean): void; ngStaticData?: never;
}; };
export type EmbeddedTemplate<T> = (ctx: T) => void; export type EmbeddedTemplate<T> = (ctx: T) => void;

View File

@ -7,9 +7,7 @@
*/ */
import {Observable} from 'rxjs/Observable'; import {Observable} from 'rxjs/Observable';
import {QueryList as IQueryList, Type} from '../core'; import {QueryList as IQueryList, Type} from '../core';
import {assertNotNull} from './assert'; import {assertNotNull} from './assert';
import {LContainer, LNode, LNodeFlags, LView, QueryState} from './interfaces'; import {LContainer, LNode, LNodeFlags, LView, QueryState} from './interfaces';

View File

@ -19,8 +19,8 @@ describe('content projection', () => {
const Child = createComponent('child', function(ctx: any, cm: boolean) { const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) { if (cm) {
m(0, dP()); m(0, dP());
E(0, 'div'); E(1, 'div');
{ P(1, 0); } { P(2, 0); }
e(); e();
} }
}); });
@ -47,7 +47,7 @@ describe('content projection', () => {
const Child = createComponent('child', function(ctx: any, cm: boolean) { const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) { if (cm) {
m(0, dP()); m(0, dP());
P(0, 0); P(1, 0);
} }
}); });
const Parent = createComponent('parent', function(ctx: any, cm: boolean) { 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) { const GrandChild = createComponent('grand-child', function(ctx: any, cm: boolean) {
if (cm) { if (cm) {
m(0, dP()); m(0, dP());
E(0, 'div'); E(1, 'div');
{ P(1, 0); } { P(2, 0); }
e(); e();
} }
}); });
const Child = createComponent('child', function(ctx: any, cm: boolean) { const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) { if (cm) {
m(0, dP()); m(0, dP());
E(0, GrandChild.ngComponentDef); E(1, GrandChild.ngComponentDef);
{ {
D(0, GrandChild.ngComponentDef.n(), GrandChild.ngComponentDef); D(0, GrandChild.ngComponentDef.n(), GrandChild.ngComponentDef);
P(1, 0); P(2, 0);
} }
e(); e();
GrandChild.ngComponentDef.r(0, 0); GrandChild.ngComponentDef.r(0, 1);
} }
}); });
const Parent = createComponent('parent', function(ctx: any, cm: boolean) { 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) { const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) { if (cm) {
m(0, dP()); m(0, dP());
E(0, 'div'); E(1, 'div');
{ P(1, 0); } { P(2, 0); }
e(); e();
} }
}); });
@ -148,12 +148,53 @@ describe('content projection', () => {
expect(toHtml(parent)).toEqual('<child><div>()</div></child>'); expect(toHtml(parent)).toEqual('<child><div>()</div></child>');
}); });
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('<child></child>');
parent.value = true;
detectChanges(parent);
expect(toHtml(parent)).toEqual('<child>content</child>');
parent.value = false;
detectChanges(parent);
expect(toHtml(parent)).toEqual('<child></child>');
});
it('should project content with container and if-else.', () => { it('should project content with container and if-else.', () => {
const Child = createComponent('child', function(ctx: any, cm: boolean) { const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) { if (cm) {
m(0, dP()); m(0, dP());
E(0, 'div'); E(1, 'div');
{ P(1, 0); } { P(2, 0); }
e(); e();
} }
}); });
@ -211,14 +252,14 @@ describe('content projection', () => {
const Child = createComponent('child', function(ctx: any, cm: boolean) { const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) { if (cm) {
m(0, dP()); m(0, dP());
E(0, 'div'); E(1, 'div');
{ {
C(1); C(2);
c(); c();
} }
e(); e();
} }
rC(1); rC(2);
{ {
if (!ctx.skipContent) { if (!ctx.skipContent) {
if (V(0)) { if (V(0)) {
@ -268,14 +309,14 @@ describe('content projection', () => {
const Child = createComponent('child', function(ctx: any, cm: boolean) { const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) { if (cm) {
m(0, dP()); m(0, dP());
E(0, 'div'); E(1, 'div');
{ {
C(1); C(2);
c(); c();
} }
e(); e();
} }
rC(1); rC(2);
{ {
if (!ctx.skipContent) { if (!ctx.skipContent) {
if (V(0)) { if (V(0)) {
@ -317,11 +358,11 @@ describe('content projection', () => {
const Child = createComponent('child', function(ctx: any, cm: boolean) { const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) { if (cm) {
m(0, dP()); m(0, dP());
E(0, 'div'); E(1, 'div');
{ P(1, 0); } { P(2, 0); }
e(); e();
E(2, 'span'); E(3, 'span');
{ P(3, 0); } { P(4, 0); }
e(); e();
} }
}); });
@ -366,15 +407,15 @@ describe('content projection', () => {
const Child = createComponent('child', function(ctx: any, cm: boolean) { const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) { if (cm) {
m(0, dP()); m(0, dP());
P(0, 0); P(1, 0);
E(1, 'div'); E(2, 'div');
{ {
C(2); C(3);
c(); c();
} }
e(); e();
} }
rC(2); rC(3);
{ {
if (ctx.show) { if (ctx.show) {
if (V(0)) { if (V(0)) {
@ -419,11 +460,11 @@ describe('content projection', () => {
if (cm) { if (cm) {
m(0, m(0,
dP([[[['span', 'title', 'toFirst'], null]], [[['span', 'title', 'toSecond'], null]]])); dP([[[['span', 'title', 'toFirst'], null]], [[['span', 'title', 'toSecond'], null]]]));
E(0, 'div', ['id', 'first']); E(1, 'div', ['id', 'first']);
{ P(1, 0, 1); } { P(2, 0, 1); }
e(); e();
E(2, 'div', ['id', 'second']); E(3, 'div', ['id', 'second']);
{ P(3, 0, 2); } { P(4, 0, 2); }
e(); e();
} }
}); });
@ -466,11 +507,11 @@ describe('content projection', () => {
if (cm) { if (cm) {
m(0, m(0,
dP([[[['span', 'class', 'toFirst'], null]], [[['span', 'class', 'toSecond'], null]]])); dP([[[['span', 'class', 'toFirst'], null]], [[['span', 'class', 'toSecond'], null]]]));
E(0, 'div', ['id', 'first']); E(1, 'div', ['id', 'first']);
{ P(1, 0, 1); } { P(2, 0, 1); }
e(); e();
E(2, 'div', ['id', 'second']); E(3, 'div', ['id', 'second']);
{ P(3, 0, 2); } { P(4, 0, 2); }
e(); e();
} }
}); });
@ -513,11 +554,11 @@ describe('content projection', () => {
if (cm) { if (cm) {
m(0, m(0,
dP([[[['span', 'class', 'toFirst'], null]], [[['span', 'class', 'toSecond'], null]]])); dP([[[['span', 'class', 'toFirst'], null]], [[['span', 'class', 'toSecond'], null]]]));
E(0, 'div', ['id', 'first']); E(1, 'div', ['id', 'first']);
{ P(1, 0, 1); } { P(2, 0, 1); }
e(); e();
E(2, 'div', ['id', 'second']); E(3, 'div', ['id', 'second']);
{ P(3, 0, 2); } { P(4, 0, 2); }
e(); e();
} }
}); });
@ -559,11 +600,11 @@ describe('content projection', () => {
const Child = createComponent('child', function(ctx: any, cm: boolean) { const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) { if (cm) {
m(0, dP([[[['span'], null]], [[['span', 'class', 'toSecond'], null]]])); m(0, dP([[[['span'], null]], [[['span', 'class', 'toSecond'], null]]]));
E(0, 'div', ['id', 'first']); E(1, 'div', ['id', 'first']);
{ P(1, 0, 1); } { P(2, 0, 1); }
e(); e();
E(2, 'div', ['id', 'second']); E(3, 'div', ['id', 'second']);
{ P(3, 0, 2); } { P(4, 0, 2); }
e(); e();
} }
}); });
@ -605,11 +646,11 @@ describe('content projection', () => {
const Child = createComponent('child', function(ctx: any, cm: boolean) { const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) { if (cm) {
m(0, dP([[[['span', 'class', 'toFirst'], null]]])); m(0, dP([[[['span', 'class', 'toFirst'], null]]]));
E(0, 'div', ['id', 'first']); E(1, 'div', ['id', 'first']);
{ P(1, 0, 1); } { P(2, 0, 1); }
e(); e();
E(2, 'div', ['id', 'second']); E(3, 'div', ['id', 'second']);
{ P(3, 0); } { P(4, 0); }
e(); e();
} }
}); });
@ -652,11 +693,11 @@ describe('content projection', () => {
const Child = createComponent('child', function(ctx: any, cm: boolean) { const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) { if (cm) {
m(0, dP([[[['span', 'class', 'toSecond'], null]]])); m(0, dP([[[['span', 'class', 'toSecond'], null]]]));
E(0, 'div', ['id', 'first']); E(1, 'div', ['id', 'first']);
{ P(1, 0); } { P(2, 0); }
e(); e();
E(2, 'div', ['id', 'second']); E(3, 'div', ['id', 'second']);
{ P(3, 0, 1); } { P(4, 0, 1); }
e(); e();
} }
}); });
@ -706,10 +747,10 @@ describe('content projection', () => {
const GrandChild = createComponent('grand-child', function(ctx: any, cm: boolean) { const GrandChild = createComponent('grand-child', function(ctx: any, cm: boolean) {
if (cm) { if (cm) {
m(0, dP([[[['span'], null]]])); m(0, dP([[[['span'], null]]]));
P(0, 0, 1); P(1, 0, 1);
E(1, 'hr'); E(2, 'hr');
e(); 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) { const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) { if (cm) {
m(0, dP()); m(0, dP());
E(0, GrandChild.ngComponentDef); E(1, GrandChild.ngComponentDef);
{ {
D(0, GrandChild.ngComponentDef.n(), GrandChild.ngComponentDef); D(0, GrandChild.ngComponentDef.n(), GrandChild.ngComponentDef);
P(1, 0); P(2, 0);
E(2, 'span'); E(3, 'span');
{ T(3, 'in child template'); } { T(4, 'in child template'); }
e(); e();
} }
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) { const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) { if (cm) {
m(0, dP([[[['div'], null]]])); m(0, dP([[[['div'], null]]]));
E(0, 'span'); E(1, 'span');
{ P(1, 0, 1); } { P(2, 0, 1); }
e(); e();
} }
}); });

View File

@ -10,7 +10,7 @@ import {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core';
import {bloomFindPossibleInjector} from '../../src/render3/di'; 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 {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 {LNodeFlags, LNodeInjector} from '../../src/render3/interfaces';
import {renderToHtml} from './render_util'; import {renderToHtml} from './render_util';
@ -321,7 +321,7 @@ describe('di', () => {
const contentView = createViewState(-1, null !); const contentView = createViewState(-1, null !);
const oldView = enterView(contentView, null !); const oldView = enterView(contentView, null !);
try { 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. // Simulate the situation where the previous parent is not initialized.
// This happens on first bootstrap because we don't init existing values // This happens on first bootstrap because we don't init existing values

View File

@ -589,4 +589,54 @@ describe('iv integration test', () => {
}); });
}); });
describe('template data', () => {
it('should re-use template data and node data', () => {
/**
* % if (condition) {
* <div></div>
* % }
*/
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);
});
});
}); });

View File

@ -6,11 +6,18 @@
* found in the LICENSE file at https://angular.io/license * 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'; import {isNodeMatchingSelector, isNodeMatchingSelectorWithNegations, isNodeMatchingSimpleSelector} from '../../src/render3/node_selector_matcher';
function testLStaticData(tagName: string, attrs: string[] | null): NodeBindings { function testLStaticData(tagName: string, attrs: string[] | null): LNodeStatic {
return {tagName, attrs, initialInputs: undefined, inputs: undefined, outputs: undefined}; return {
tagName,
attrs,
initialInputs: undefined,
inputs: undefined,
outputs: undefined,
containerStatic: null
};
} }
describe('css selector matching', () => { describe('css selector matching', () => {

View File

@ -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', () => { it('should call component output function when event is emitted', () => {
/** <button-toggle (change)="onChange()"></button-toggle> */ /** <button-toggle (change)="onChange()"></button-toggle> */
@ -328,15 +339,6 @@ describe('outputs', () => {
}); });
it('should work with two outputs of the same name', () => { 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'}});
}
/** <button-toggle (change)="onChange()" otherDir></button-toggle> */ /** <button-toggle (change)="onChange()" otherDir></button-toggle> */
function Template(ctx: any, cm: boolean) { function Template(ctx: any, cm: boolean) {
if (cm) { if (cm) {
@ -357,7 +359,7 @@ describe('outputs', () => {
buttonToggle !.change.next(); buttonToggle !.change.next();
expect(counter).toEqual(1); expect(counter).toEqual(1);
otherDir !.change.next(); otherDir !.changeStream.next();
expect(counter).toEqual(2); expect(counter).toEqual(2);
}); });
@ -397,4 +399,67 @@ describe('outputs', () => {
expect(counter).toEqual(1); expect(counter).toEqual(1);
}); });
it('should work with outputs at same index in if block', () => {
/**
* <button (click)="onClick()">Click me</button> // outputs: null
* % if (condition) {
* <button-toggle (change)="onChange()"></button-toggle> // outputs: {change: [0, 'change']}
* % } else {
* <div otherDir (change)="onChange()"></div> // 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);
});
}); });

View File

@ -239,6 +239,73 @@ describe('elementProperty', () => {
renderToHtml(Template, ctx); renderToHtml(Template, ctx);
expect(otherDir !.id).toEqual(2); 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'}});
}
/**
* <button idDir [id]="id1">Click me</button> // inputs: {'id': [0, 'idNumber']}
* % if (condition) {
* <button [id]="id2">Click me too</button> // inputs: null
* % } else {
* <button otherDir [id]="id3">Click me too</button> // 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(`<button>Click me</button><button id="two">Click me too</button>`);
expect(idDir !.idNumber).toEqual('one');
expect(renderToHtml(Template, {condition: false, id1: 'four', id2: 'two', id3: 'three'}))
.toEqual(`<button>Click me</button><button>Click me too</button>`);
expect(idDir !.idNumber).toEqual('four');
expect(otherDir !.id).toEqual('three');
});
}); });
describe('attributes and input properties', () => { describe('attributes and input properties', () => {
@ -382,6 +449,58 @@ describe('elementProperty', () => {
expect(dirB !.roleB).toEqual('listbox'); expect(dirB !.roleB).toEqual('listbox');
}); });
it('should support attributes at same index inside an if-else block', () => {
/**
* <div role="listbox" myDir></div> // initialInputs: [['role', 'listbox']]
*
* % if (condition) {
* <div role="button" myDirB></div> // initialInputs: [['role', 'button']]
* % } else {
* <div role="menu"></div> // 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(`<div role="listbox"></div><div role="button"></div>`);
expect(myDir !.role).toEqual('listbox');
expect(dirB !.roleB).toEqual('button');
expect((dirB !as any).role).toBeUndefined();
expect(renderToHtml(Template, {
condition: false
})).toEqual(`<div role="listbox"></div><div role="menu"></div>`);
expect(myDir !.role).toEqual('listbox');
});
it('should process attributes properly inside a for loop', () => { it('should process attributes properly inside a for loop', () => {
class Comp { class Comp {

View File

@ -31,10 +31,10 @@ describe('query', () => {
if (cm) { if (cm) {
m(0, Q(Child, false)); m(0, Q(Child, false));
m(1, Q(Child, true)); m(1, Q(Child, true));
E(0, Child.ngComponentDef); E(2, Child.ngComponentDef);
{ {
child1 = D(0, Child.ngComponentDef.n(), 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); } { child2 = D(1, Child.ngComponentDef.n(), Child.ngComponentDef); }
e(); e();
} }

View File

@ -7,7 +7,7 @@
*/ */
import {ComponentTemplate, ComponentType, PublicFeature, defineComponent, renderComponent as _renderComponent} from '../../src/render3/index'; 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 {LElement, LNodeFlags} from '../../src/render3/interfaces';
import {RElement, RText, Renderer3} from '../../src/render3/renderer'; import {RElement, RText, Renderer3} from '../../src/render3/renderer';
import {getRenderer2} from './imported_renderer2'; import {getRenderer2} from './imported_renderer2';
@ -37,7 +37,7 @@ export function resetDOM() {
requestAnimationFrame.queue = []; requestAnimationFrame.queue = [];
containerEl = document.createElement('div'); containerEl = document.createElement('div');
containerEl.setAttribute('host', ''); 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) // TODO: assert that the global state is clean (e.g. ngData, previousOrParentNode, etc)
} }