diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index c8a9d0925d..3704c2b7f2 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -62,7 +62,6 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver private _bindingCode: o.Statement[] = []; private _postfixCode: o.Statement[] = []; private _temporary = temporaryAllocator(this._prefixCode, TEMPORARY_NAME); - private _projectionDefinitionIndex = -1; private _valueConverter: ValueConverter; private _unsupported = unsupported; private _bindingScope: BindingScope; @@ -121,8 +120,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // Output a `ProjectionDef` instruction when some `` are present if (hasNgContent) { - this._projectionDefinitionIndex = this.allocateDataSlot(); - const parameters: o.Expression[] = [o.literal(this._projectionDefinitionIndex)]; + const parameters: o.Expression[] = []; // Only selectors with a non-default value are generated if (ngContentSelectors.length > 1) { @@ -217,10 +215,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver visitContent(ngContent: t.Content) { const slot = this.allocateDataSlot(); const selectorIndex = ngContent.selectorIndex; - const parameters: o.Expression[] = [ - o.literal(slot), - o.literal(this._projectionDefinitionIndex), - ]; + const parameters: o.Expression[] = [o.literal(slot)]; const attributeAsList: string[] = []; diff --git a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts index df7d0fbca4..47a041a224 100644 --- a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts +++ b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts @@ -871,9 +871,9 @@ describe('compiler compliance', () => { factory: function SimpleComponent_Factory() { return new SimpleComponent(); }, template: function SimpleComponent_Template(rf: IDENT, ctx: IDENT) { if (rf & 1) { - $r3$.ɵpD(0); - $r3$.ɵE(1, 'div'); - $r3$.ɵP(2, 0); + $r3$.ɵpD(); + $r3$.ɵE(0, 'div'); + $r3$.ɵP(1); $r3$.ɵe(); } } @@ -891,12 +891,12 @@ describe('compiler compliance', () => { factory: function ComplexComponent_Factory() { return new ComplexComponent(); }, template: function ComplexComponent_Template(rf: IDENT, ctx: IDENT) { if (rf & 1) { - $r3$.ɵpD(0, $c1$, $c2$); - $r3$.ɵE(1, 'div', $c3$); - $r3$.ɵP(2, 0, 1); + $r3$.ɵpD($c1$, $c2$); + $r3$.ɵE(0, 'div', $c3$); + $r3$.ɵP(1, 1); $r3$.ɵe(); - $r3$.ɵE(3, 'div', $c4$); - $r3$.ɵP(4, 0, 2); + $r3$.ɵE(2, 'div', $c4$); + $r3$.ɵP(3, 2); $r3$.ɵe(); } } @@ -1022,9 +1022,9 @@ describe('compiler compliance', () => { template: function ContentQueryComponent_Template( rf: $RenderFlags$, ctx: $ContentQueryComponent$) { if (rf & 1) { - $r3$.ɵpD(0); - $r3$.ɵE(1, 'div'); - $r3$.ɵP(2, 0); + $r3$.ɵpD(); + $r3$.ɵE(0, 'div'); + $r3$.ɵP(1); $r3$.ɵe(); } } diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts index 4262df274a..fdcbee7dff 100644 --- a/packages/core/src/render3/i18n.ts +++ b/packages/core/src/render3/i18n.ts @@ -9,7 +9,7 @@ import {assertEqual, assertLessThan} from './assert'; import {NO_CHANGE, bindingUpdated, createLNode, getPreviousOrParentNode, getRenderer, getViewData, load, resetApplicationState} from './instructions'; import {RENDER_PARENT} from './interfaces/container'; -import {LContainerNode, LElementNode, LNode, TContainerNode, TNodeType} from './interfaces/node'; +import {LContainerNode, LElementNode, LNode, TContainerNode, TElementNode, TNodeType} from './interfaces/node'; import {BINDING_INDEX, HEADER_OFFSET, TVIEW} from './interfaces/view'; import {appendChild, createTextNode, getParentLNode, removeChild} from './node_manipulation'; import {stringify} from './util'; @@ -243,14 +243,17 @@ function appendI18nNode(node: LNode, parentNode: LNode, previousNode: LNode) { // On first pass, re-organize node tree to put this node in the correct position. const firstTemplatePass = node.view[TVIEW].firstTemplatePass; if (firstTemplatePass) { - node.tNode.next = null; if (previousNode === parentNode && node.tNode !== parentNode.tNode.child) { node.tNode.next = parentNode.tNode.child; parentNode.tNode.child = node.tNode; } else if (previousNode !== parentNode && node.tNode !== previousNode.tNode.next) { node.tNode.next = previousNode.tNode.next; previousNode.tNode.next = node.tNode; + } else { + node.tNode.next = null; } + + if (parentNode.view === node.view) node.tNode.parent = parentNode.tNode as TElementNode; } // Template containers also have a comment node for the `ViewContainerRef` that should be moved diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 3725b08f7b..ff42452007 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -14,20 +14,20 @@ import {assertDefined, assertEqual, assertLessThan, assertNotDefined, assertNotE import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors'; import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks'; import {ACTIVE_INDEX, LContainer, RENDER_PARENT, VIEWS} from './interfaces/container'; +import {ComponentDefInternal, ComponentQuery, ComponentTemplate, DirectiveDefInternal, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; import {LInjector} from './interfaces/injector'; -import {CssSelectorList, LProjection, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'; +import {AttributeMarker, InitialInputData, InitialInputs, LContainerNode, LElementNode, LNode, LProjectionNode, LTextNode, LViewNode, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node'; +import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'; import {LQueries} from './interfaces/query'; +import {ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer'; import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTEXT, CurrentMatchesList, DIRECTIVES, FLAGS, HEADER_OFFSET, HOST_NODE, INJECTOR, LViewData, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RootContext, SANITIZER, TAIL, TData, TVIEW, TView} from './interfaces/view'; - -import {AttributeMarker, TAttributes, LContainerNode, LElementNode, LNode, TNodeType, TNodeFlags, LProjectionNode, LTextNode, LViewNode, TNode, TContainerNode, InitialInputData, InitialInputs, PropertyAliases, PropertyAliasValue, TElementNode,} from './interfaces/node'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; -import {appendChild, insertView, appendProjectedNode, removeView, canInsertNativeNode, createTextNode, getNextLNode, getChildLNode, getParentLNode, getLViewChild} from './node_manipulation'; +import {appendChild, appendProjectedNode, canInsertNativeNode, createTextNode, findComponentHost, getChildLNode, getLViewChild, getNextLNode, getParentLNode, insertView, removeView} from './node_manipulation'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; -import {ComponentDefInternal, ComponentTemplate, ComponentQuery, DirectiveDefInternal, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; -import {RComment, RElement, RText, Renderer3, RendererFactory3, ProceduralRenderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces/renderer'; +import {StylingContext, allocStylingContext, createStylingContextTemplate, renderStyles as renderElementStyles, updateStyleMap as updateElementStyleMap, updateStyleProp as updateElementStyleProp} from './styling'; import {isDifferent, stringify} from './util'; import {ViewRef} from './view_ref'; -import {StylingContext, allocStylingContext, createStylingContextTemplate, updateStyleMap as updateElementStyleMap, updateStyleProp as updateElementStyleProp, renderStyles as renderElementStyles} from './styling'; + /** * Directive (D) sets a property on all component instances using this constant as a key and the @@ -335,7 +335,6 @@ export function createLNodeObject( data: state, queries: queries, tNode: null !, - pNextOrParent: null, dynamicLContainerNode: null }; } @@ -363,11 +362,11 @@ export function createLNode( attrs: TAttributes | null, lContainer: LContainer): LContainerNode; export function createLNode( index: number, type: TNodeType.Projection, native: null, name: null, attrs: TAttributes | null, - lProjection: LProjection): LProjectionNode; + lProjection: null): LProjectionNode; export function createLNode( index: number, type: TNodeType, native: RText | RElement | RComment | null, name: string | null, - attrs: TAttributes | null, state?: null | LViewData | LContainer | LProjection): LElementNode& - LTextNode&LViewNode&LContainerNode&LProjectionNode { + attrs: TAttributes | null, state?: null | LViewData | LContainer): LElementNode<extNode& + LViewNode&LContainerNode&LProjectionNode { const parent = isParent ? previousOrParentNode : previousOrParentNode && getParentLNode(previousOrParentNode) !as LNode; // Parents cannot cross component boundaries because components will be used in multiple places, @@ -1189,7 +1188,8 @@ export function createTNode( parent: parent, dynamicContainerNode: null, detached: null, - stylingTemplate: null + stylingTemplate: null, + projection: null }; } @@ -1948,138 +1948,109 @@ export function viewAttached(view: LViewData): boolean { * @param selectors A collection of parsed CSS selectors * @param rawSelectors A collection of CSS selectors in the raw, un-parsed form */ -export function projectionDef( - index: number, selectors?: CssSelectorList[], textSelectors?: string[]): void { - const noOfNodeBuckets = selectors ? selectors.length + 1 : 1; - const distributedNodes = new Array(noOfNodeBuckets); - for (let i = 0; i < noOfNodeBuckets; i++) { - distributedNodes[i] = []; - } - +export function projectionDef(selectors?: CssSelectorList[], textSelectors?: string[]): void { const componentNode: LElementNode = findComponentHost(viewData); - let componentChild = getChildLNode(componentNode); - while (componentChild !== null) { - // execute selector matching logic if and only if: - // - there are selectors defined - // - a node has a tag name / attributes that can be matched - const bucketIndex = - selectors ? matchingSelectorIndex(componentChild.tNode, selectors, textSelectors !) : 0; - distributedNodes[bucketIndex].push(componentChild); + if (!componentNode.tNode.projection) { + const noOfNodeBuckets = selectors ? selectors.length + 1 : 1; + const pData: (TNode | null)[] = componentNode.tNode.projection = + new Array(noOfNodeBuckets).fill(null); + const tails: (TNode | null)[] = pData.slice(); - componentChild = getNextLNode(componentChild); + let componentChild = componentNode.tNode.child; + + while (componentChild !== null) { + const bucketIndex = + selectors ? matchingSelectorIndex(componentChild, selectors, textSelectors !) : 0; + const nextNode = componentChild.next; + + if (tails[bucketIndex]) { + tails[bucketIndex] !.next = componentChild; + } else { + pData[bucketIndex] = componentChild; + componentChild.next = null; + } + tails[bucketIndex] = componentChild; + + componentChild = nextNode; + } } - - ngDevMode && assertDataNext(index + HEADER_OFFSET); - store(index, distributedNodes); } /** - * Updates the linked list of a projection node, by appending another linked list. + * Stack used to keep track of projection nodes in projection() instruction. * - * @param projectionNode Projection node whose projected nodes linked list has to be updated - * @param appendedFirst First node of the linked list to append. - * @param appendedLast Last node of the linked list to append. + * This is deliberately created outside of projection() to avoid allocating + * a new array each time the function is called. Instead the array will be + * re-used by each invocation. This works because the function is not reentrant. */ -function addToProjectionList( - projectionNode: LProjectionNode, - appendedFirst: LElementNode | LTextNode | LContainerNode | null, - appendedLast: LElementNode | LTextNode | LContainerNode | null) { - ngDevMode && assertEqual( - !!appendedFirst, !!appendedLast, - 'appendedFirst can be null if and only if appendedLast is also null'); - if (!appendedLast) { - // nothing to append - return; - } - const projectionNodeData = projectionNode.data; - if (projectionNodeData.tail) { - projectionNodeData.tail.pNextOrParent = appendedFirst; - } else { - projectionNodeData.head = appendedFirst; - } - projectionNodeData.tail = appendedLast; - appendedLast.pNextOrParent = projectionNode; -} +const projectionNodeStack: LProjectionNode[] = []; /** * Inserts previously re-distributed projected nodes. This instruction must be preceded by a call * to the projectionDef instruction. * * @param nodeIndex - * @param localIndex - index under which distribution of projected nodes was memorized * @param selectorIndex: * - 0 when the selector is `*` (or unspecified as this is the default value), * - 1 based index of the selector from the {@link projectionDef} */ -export function projection( - nodeIndex: number, localIndex: number, selectorIndex: number = 0, attrs?: string[]): void { - const node = createLNode( - nodeIndex, TNodeType.Projection, null, null, attrs || null, {head: null, tail: null}); +export function projection(nodeIndex: number, selectorIndex: number = 0, attrs?: string[]): void { + const node = createLNode(nodeIndex, TNodeType.Projection, null, null, attrs || null, null); + + // We can't use viewData[HOST_NODE] because projection nodes can be nested in embedded views. + if (node.tNode.projection === null) node.tNode.projection = selectorIndex; // `` has no content isParent = false; - // re-distribution of projectable nodes is memorized on a component's view level - const componentNode = findComponentHost(viewData); - const componentLView = componentNode.data as LViewData; - const distributedNodes = loadInternal(localIndex, componentLView) as Array; - const nodesForSelector = distributedNodes[selectorIndex]; + // re-distribution of projectable nodes is stored on a component's view level + const parent = getParentLNode(node); - const currentParent = getParentLNode(node); - const canInsert = canInsertNativeNode(currentParent, viewData); - let grandparent: LContainerNode; - const renderParent = currentParent.tNode.type === TNodeType.View ? - (grandparent = getParentLNode(currentParent) as LContainerNode) && - grandparent.data[RENDER_PARENT] ! : - currentParent as LElementNode; + if (canInsertNativeNode(parent, viewData)) { + const componentNode = findComponentHost(viewData); + let nodeToProject = (componentNode.tNode.projection as(TNode | null)[])[selectorIndex]; + let projectedView = componentNode.view; + let projectionNodeIndex = -1; + let grandparent: LContainerNode; + const renderParent = parent.tNode.type === TNodeType.View ? + (grandparent = getParentLNode(parent) as LContainerNode) && + grandparent.data[RENDER_PARENT] ! : + parent as LElementNode; - for (let i = 0; i < nodesForSelector.length; i++) { - const nodeToProject = nodesForSelector[i]; - let head = nodeToProject as LTextNode | LElementNode | LContainerNode | null; - let tail = nodeToProject as LTextNode | LElementNode | LContainerNode | null; + while (nodeToProject) { + if (nodeToProject.type === TNodeType.Projection) { + // This node is re-projected, so we must go up the tree to get its projected nodes. + const currentComponentHost = findComponentHost(projectedView); + const firstProjectedNode = (currentComponentHost.tNode.projection as( + TNode | null)[])[nodeToProject.projection as number]; - if (nodeToProject.tNode.type === TNodeType.Projection) { - const previouslyProjected = (nodeToProject as LProjectionNode).data; - head = previouslyProjected.head; - tail = previouslyProjected.tail; - } - - addToProjectionList(node, head, tail); - - if (canInsert) { - let currentNode: LNode|null = head; - while (currentNode) { + if (firstProjectedNode) { + projectionNodeStack[++projectionNodeIndex] = projectedView[nodeToProject.index]; + nodeToProject = firstProjectedNode; + projectedView = currentComponentHost.view; + continue; + } + } else { + const lNode = projectedView[nodeToProject.index]; + lNode.tNode.flags |= TNodeFlags.isProjected; appendProjectedNode( - currentNode as LTextNode | LElementNode | LContainerNode, currentParent, viewData, - renderParent); - currentNode = currentNode === tail ? null : currentNode.pNextOrParent; + lNode as LTextNode | LElementNode | LContainerNode, parent, viewData, renderParent); } + + // If we are finished with a list of re-projected nodes, we need to get + // back to the root projection node that was re-projected. + if (nodeToProject.next === null && projectedView !== componentNode.view) { + // move down into the view of the component we're projecting right now + const lNode = projectionNodeStack[projectionNodeIndex--]; + nodeToProject = lNode.tNode; + projectedView = lNode.view; + } + nodeToProject = nodeToProject.next; } } } -/** - * Given a current view, finds the nearest component's host (LElement). - * - * @param lViewData LViewData for which we want a host element node - * @returns The host node - */ -function findComponentHost(lViewData: LViewData): LElementNode { - let viewRootLNode = lViewData[HOST_NODE]; - - while (viewRootLNode.tNode.type === TNodeType.View) { - ngDevMode && assertDefined(lViewData[PARENT], 'lViewData.parent'); - lViewData = lViewData[PARENT] !; - viewRootLNode = lViewData[HOST_NODE]; - } - - ngDevMode && assertNodeType(viewRootLNode, TNodeType.Element); - ngDevMode && assertDefined(viewRootLNode.data, 'node.data'); - - return viewRootLNode as LElementNode; -} - /** * Adds LViewData or LContainer to the end of the current view tree. * diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index 29893c73d9..817c970d13 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -10,7 +10,6 @@ import {StylingContext} from '../styling'; import {LContainer} from './container'; import {LInjector} from './injector'; -import {LProjection} from './projection'; import {LQueries} from './query'; import {RComment, RElement, RText} from './renderer'; import {LViewData, TView} from './view'; @@ -36,11 +35,14 @@ export const enum TNodeFlags { /** The number of directives on this node is encoded on the least significant bits */ DirectiveCountMask = 0b00000000000000000000111111111111, - /** Then this bit is set when the node is a component */ - isComponent = 0b1000000000000, + /** This bit is set if the node is a component */ + isComponent = 0b00000000000000000001000000000000, + + /** This bit is set if the node has been projected */ + isProjected = 0b00000000000000000010000000000000, /** The index of the first directive on this node is encoded on the most significant bits */ - DirectiveStartingIndexShift = 13, + DirectiveStartingIndexShift = 14, } /** @@ -74,7 +76,7 @@ export interface LNode { * If LContainerNode, then `data` contains LContainer. * If LProjectionNode, then `data` contains LProjection. */ - readonly data: LViewData|LContainer|LProjection|null; + readonly data: LViewData|LContainer|null; /** @@ -94,14 +96,6 @@ export interface LNode { */ queries: LQueries|null; - /** - * If this node is projected, pointer to the next node in the same projection parent - * (which is a container, an element, or a text node), or to the parent projection node - * if this is the last node in the projection. - * If this node is not projected, this field is null. - */ - pNextOrParent: LNode|null; - /** * Pointer to the corresponding TNode object, which stores static * data about this node. @@ -156,7 +150,7 @@ export interface LContainerNode extends LNode { export interface LProjectionNode extends LNode { readonly native: null; - readonly data: LProjection; + readonly data: null; dynamicLContainerNode: null; } @@ -345,6 +339,43 @@ export interface TNode { detached: boolean|null; stylingTemplate: StylingContext|null; + /** + * List of projected TNodes for a given component host element OR index into the said nodes. + * + * For easier discussion assume this example: + * ``'s view definition: + * ``` + * content1 + * content2 + * ``` + * ``'s view definition: + * ``` + * + * ``` + * + * If `Array.isArray(projection)` then `TNode` is a host element: + * - `projection` stores the content nodes which are to be projected. + * - The nodes represent categories defined by the selector: For example: + * `` would represent the heads for `` + * and `` respectively. + * - The nodes we store in `projection` are heads only, we used `.next` to get their + * siblings. + * - The nodes `.next` is sorted/rewritten as part of the projection setup. + * - `projection` size is equal to the number of projections ``. The size of + * `c1` will be `1` because `` has only one ``. + * - we store `projection` with the host (`c1`, `c2`) rather than the `` (`cont1`) + * because the same component (``) can be used in multiple locations (`c1`, `c2`) and as + * a result have different set of nodes to project. + * - without `projection` it would be difficult to efficiently traverse nodes to be projected. + * + * If `typeof projection == 'number'` then `TNode` is a `` element: + * - `projection` is an index of the host's `projection`Nodes. + * - This would return the first head node to project: + * `getHost(currentTNode).projection[currentTNode.projection]`. + * - When projecting nodes the parent node retrieved may be a `` node, in which case + * the process is recursive in nature (not implementation). + */ + projection: (TNode|null)[]|number|null; } /** Static data for an LElementNode */ @@ -359,6 +390,13 @@ export interface TElementNode extends TNode { */ parent: TElementNode|null; tViews: null; + + /** + * If this is a component TNode with projection, this will be an array of projected + * TNodes (see TNode.projection for more info). If it's a regular element node or a + * component without projection, it will be null. + */ + projection: (TNode|null)[]|null; } /** Static data for an LTextNode */ @@ -373,6 +411,7 @@ export interface TTextNode extends TNode { */ parent: TElementNode|null; tViews: null; + projection: null; } /** Static data for an LContainerNode */ @@ -394,6 +433,7 @@ export interface TContainerNode extends TNode { */ parent: TElementNode|null; tViews: TView|TView[]|null; + projection: null; } /** Static data for an LViewNode */ @@ -403,6 +443,7 @@ export interface TViewNode extends TNode { child: TElementNode|TTextNode|TContainerNode|TProjectionNode|null; parent: TContainerNode|null; tViews: null; + projection: null; } /** Static data for an LProjectionNode */ @@ -416,6 +457,9 @@ export interface TProjectionNode extends TNode { */ parent: TElementNode|null; tViews: null; + + /** Index of the projection node. (See TNode.projection for more info.) */ + projection: number; } /** diff --git a/packages/core/src/render3/interfaces/projection.ts b/packages/core/src/render3/interfaces/projection.ts index dce0453813..beffaa06db 100644 --- a/packages/core/src/render3/interfaces/projection.ts +++ b/packages/core/src/render3/interfaces/projection.ts @@ -1,3 +1,4 @@ + /** * @license * Copyright Google Inc. All Rights Reserved. @@ -6,15 +7,6 @@ * found in the LICENSE file at https://angular.io/license */ -import {LContainerNode, LElementNode, LTextNode} from './node'; - -/** - * Linked list of projected nodes (using the pNextOrParent property). - */ -export interface LProjection { - head: LElementNode|LTextNode|LContainerNode|null; - tail: LElementNode|LTextNode|LContainerNode|null; -} /** * Expresses a single CSS Selector. diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 95a4a35715..41c9f14cc5 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -6,9 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ +import {assertDefined} from './assert'; import {callHooks} from './hooks'; import {LContainer, RENDER_PARENT, VIEWS, unusedValueExportToPlacateAjd as unused1} from './interfaces/container'; -import {LContainerNode, LElementNode, LNode, LProjectionNode, LTextNode, LViewNode, TNodeType, unusedValueExportToPlacateAjd as unused2} from './interfaces/node'; +import {LContainerNode, LElementNode, LNode, LProjectionNode, LTextNode, LViewNode, TNode, TNodeFlags, TNodeType, unusedValueExportToPlacateAjd as unused2} from './interfaces/node'; import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection'; import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer'; import {CLEANUP, CONTAINER_INDEX, DIRECTIVES, FLAGS, HEADER_OFFSET, HOST_NODE, HookData, LViewData, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, TVIEW, unusedValueExportToPlacateAjd as unused5} from './interfaces/view'; @@ -24,7 +25,7 @@ export function getNextLNode(node: LNode): LNode|null { const viewData = node.data as LViewData; return viewData[NEXT] ? (viewData[NEXT] as LViewData)[HOST_NODE] : null; } - return node.tNode.next ? node.view[node.tNode.next !.index] : null; + return node.tNode.next ? node.view[node.tNode.next.index] : null; } /** Retrieves the first child of a given node */ @@ -52,27 +53,6 @@ export function getParentLNode(node: LNode): LElementNode|LContainerNode|LViewNo return parent ? node.view[parent.index] : node.view[HOST_NODE]; } -/** - * Get the next node in the LNode tree, taking into account the place where a node is - * projected (in the shadow DOM) rather than where it comes from (in the light DOM). - * - * @param node The node whose next node in the LNode tree must be found. - * @return LNode|null The next sibling in the LNode tree. - */ -function getNextLNodeWithProjection(node: LNode): LNode|null { - const pNextOrParent = node.pNextOrParent; - - if (pNextOrParent) { - // The node is projected - const isLastProjectedNode = pNextOrParent.tNode.type === TNodeType.Projection; - // returns pNextOrParent if we are not at the end of the list, null otherwise - return isLastProjectedNode ? null : pNextOrParent; - } - - // returns node.next because the the node is not projected - return getNextLNode(node); -} - const enum WalkLNodeTreeAction { /** node insert in the native environment */ Insert = 0, @@ -84,12 +64,22 @@ const enum WalkLNodeTreeAction { Destroy = 2, } + +/** + * Stack used to keep track of projection nodes in walkLNodeTree. + * + * This is deliberately created outside of walkLNodeTree to avoid allocating + * a new array each time the function is called. Instead the array will be + * re-used by each invocation. This works because the function is not reentrant. + */ +const projectionNodeStack: LProjectionNode[] = []; + /** * Walks a tree of LNodes, applying a transformation on the LElement nodes, either only on the first * one found, or on all of them. * * @param startingNode the node from which the walk is started. - * @param rootNode the root node considered. + * @param rootNode the root node considered. This prevents walking past that node. * @param action identifies the action to be performed on the LElement nodes. * @param renderer the current renderer. * @param renderParentNode Optional the render parent node to be set in all LContainerNodes found, @@ -101,18 +91,19 @@ function walkLNodeTree( startingNode: LNode | null, rootNode: LNode, action: WalkLNodeTreeAction, renderer: Renderer3, renderParentNode?: LElementNode | null, beforeNode?: RNode | null) { let node: LNode|null = startingNode; + let projectionNodeIndex = -1; while (node) { let nextNode: LNode|null = null; const parent = renderParentNode ? renderParentNode.native : null; - if (node.tNode.type === TNodeType.Element) { + const nodeType = node.tNode.type; + if (nodeType === TNodeType.Element) { // Execute the action executeNodeAction(action, renderer, parent, node.native !, beforeNode); if (node.dynamicLContainerNode) { executeNodeAction( action, renderer, parent, node.dynamicLContainerNode.native !, beforeNode); } - nextNode = getNextLNode(node); - } else if (node.tNode.type === TNodeType.Container) { + } else if (nodeType === TNodeType.Container) { executeNodeAction(action, renderer, parent, node.native !, beforeNode); const lContainerNode: LContainerNode = (node as LContainerNode); const childContainerData: LContainer = lContainerNode.dynamicLContainerNode ? @@ -130,15 +121,26 @@ function walkLNodeTree( lContainerNode.dynamicLContainerNode.native : lContainerNode.native; } - } else if (node.tNode.type === TNodeType.Projection) { - // For Projection look at the first projected node - nextNode = (node as LProjectionNode).data.head; + } else if (nodeType === TNodeType.Projection) { + const componentHost = findComponentHost(node.view); + const head = + (componentHost.tNode.projection as(TNode | null)[])[node.tNode.projection as number]; + + projectionNodeStack[++projectionNodeIndex] = node as LProjectionNode; + + nextNode = head ? (componentHost.data as LViewData)[PARENT] ![head.index] : null; } else { // Otherwise look at the first child nextNode = getChildLNode(node as LViewNode); } - if (nextNode == null) { + if (nextNode === null) { + nextNode = getNextLNode(node); + + // this last node was projected, we need to get back down to its projection node + if (nextNode === null && (node.tNode.flags & TNodeFlags.isProjected)) { + nextNode = getNextLNode(projectionNodeStack[projectionNodeIndex--] as LNode); + } /** * Find the next node in the LNode tree, taking into account the place where a node is * projected (in the shadow DOM) rather than where it comes from (in the light DOM). @@ -146,29 +148,43 @@ function walkLNodeTree( * If there is no sibling node, then it goes to the next sibling of the parent node... * until it reaches rootNode (at which point null is returned). */ - let currentNode: LNode|null = node; - node = getNextLNodeWithProjection(currentNode); - while (currentNode && !node) { - // if node.pNextOrParent is not null here, it is not the next node - // (because, at this point, nextNode is null, so it is the parent) - currentNode = currentNode.pNextOrParent || getParentLNode(currentNode); - if (currentNode === rootNode) { - return null; + while (node && !nextNode) { + node = getParentLNode(node); + if (node === null || node === rootNode) return null; + + // When exiting a container, the beforeNode must be restored to the previous value + if (!node.tNode.next && nodeType === TNodeType.Container) { + beforeNode = node.native; } - // When the walker exits a container, the beforeNode has to be restored to the previous - // value. - if (currentNode && !currentNode.pNextOrParent && - currentNode.tNode.type === TNodeType.Container) { - beforeNode = currentNode.native; - } - node = currentNode && getNextLNodeWithProjection(currentNode); + nextNode = getNextLNode(node); } - } else { - node = nextNode; } + node = nextNode; } } + +/** + * Given a current view, finds the nearest component's host (LElement). + * + * @param lViewData LViewData for which we want a host element node + * @returns The host node + */ +export function findComponentHost(lViewData: LViewData): LElementNode { + let viewRootLNode = lViewData[HOST_NODE]; + + while (viewRootLNode.tNode.type === TNodeType.View) { + ngDevMode && assertDefined(lViewData[PARENT], 'lViewData.parent'); + lViewData = lViewData[PARENT] !; + viewRootLNode = lViewData[HOST_NODE]; + } + + ngDevMode && assertNodeType(viewRootLNode, TNodeType.Element); + ngDevMode && assertDefined(viewRootLNode.data, 'node.data'); + + return viewRootLNode as LElementNode; +} + /** * NOTE: for performance reasons, the possible actions are inlined within the function instead of * being passed as an argument. diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 890b47afb7..07a77163ed 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -422,6 +422,9 @@ { "name": "findAttrIndexInNode" }, + { + "name": "findComponentHost" + }, { "name": "findDirectiveMatches" }, @@ -452,9 +455,6 @@ { "name": "getNextLNode" }, - { - "name": "getNextLNodeWithProjection" - }, { "name": "getOrCreateContainerRef" }, @@ -605,6 +605,9 @@ { "name": "notImplemented" }, + { + "name": "projectionNodeStack" + }, { "name": "queueComponentIndexForCheck" }, diff --git a/packages/core/test/render3/compiler_canonical/content_projection_spec.ts b/packages/core/test/render3/compiler_canonical/content_projection_spec.ts index c7a35758c0..328c32996c 100644 --- a/packages/core/test/render3/compiler_canonical/content_projection_spec.ts +++ b/packages/core/test/render3/compiler_canonical/content_projection_spec.ts @@ -27,9 +27,9 @@ describe('content projection', () => { factory: () => new SimpleComponent(), template: function(rf: $RenderFlags$, ctx: $SimpleComponent$) { if (rf & 1) { - $r3$.ɵpD(0); - $r3$.ɵEe(1, 'div'); - $r3$.ɵP(2, 0); + $r3$.ɵpD(); + $r3$.ɵEe(0, 'div'); + $r3$.ɵP(1); } } }); @@ -56,11 +56,11 @@ describe('content projection', () => { factory: () => new ComplexComponent(), template: function(rf: $RenderFlags$, ctx: $ComplexComponent$) { if (rf & 1) { - $r3$.ɵpD(0, $pD_0P$, $pD_0R$); - $r3$.ɵEe(1, 'div', ['id', 'first']); - $r3$.ɵP(2, 0, 1); - $r3$.ɵEe(3, 'div', ['id', 'second']); - $r3$.ɵP(4, 0, 2); + $r3$.ɵpD($pD_0P$, $pD_0R$); + $r3$.ɵEe(0, 'div', ['id', 'first']); + $r3$.ɵP(1, 1); + $r3$.ɵEe(2, 'div', ['id', 'second']); + $r3$.ɵP(3, 2); } } }); diff --git a/packages/core/test/render3/compiler_canonical/query_spec.ts b/packages/core/test/render3/compiler_canonical/query_spec.ts index 3946badeb4..d8fafb9b72 100644 --- a/packages/core/test/render3/compiler_canonical/query_spec.ts +++ b/packages/core/test/render3/compiler_canonical/query_spec.ts @@ -124,9 +124,9 @@ describe('queries', () => { template: function ContentQueryComponent_Template( rf: $number$, ctx: $ContentQueryComponent$) { if (rf & 1) { - $r3$.ɵpD(0); - $r3$.ɵE(1, 'div'); - $r3$.ɵP(2, 0); + $r3$.ɵpD(); + $r3$.ɵE(0, 'div'); + $r3$.ɵP(1); $r3$.ɵe(); } } diff --git a/packages/core/test/render3/content_spec.ts b/packages/core/test/render3/content_spec.ts index 2944e11d85..e1b58224b8 100644 --- a/packages/core/test/render3/content_spec.ts +++ b/packages/core/test/render3/content_spec.ts @@ -8,6 +8,9 @@ import {SelectorFlags} from '@angular/core/src/render3/interfaces/projection'; +import {Input, TemplateRef, ViewContainerRef, ViewRef} from '../../src/core'; +import {defineDirective} from '../../src/render3/definition'; +import {injectTemplateRef, injectViewContainerRef} from '../../src/render3/di'; import {AttributeMarker, detectChanges} from '../../src/render3/index'; import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, loadDirective, projection, projectionDef, text} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; @@ -22,9 +25,9 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'div'); - { projection(2, 0); } + projectionDef(); + elementStart(0, 'div'); + { projection(1); } elementEnd(); } }); @@ -48,8 +51,8 @@ describe('content projection', () => { /** */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - projection(1, 0); + projectionDef(); + projection(0); } }); @@ -66,13 +69,47 @@ describe('content projection', () => { expect(toHtml(parent)).toEqual('content'); }); + it('should project content with siblings', () => { + /** */ + const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + projectionDef(); + projection(0); + } + }); + + /** + * + * before + *
content
+ * after + *
+ */ + const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart(0, 'child'); + { + text(1, 'before'); + elementStart(2, 'div'); + { text(3, 'content'); } + elementEnd(); + text(4, 'after'); + } + elementEnd(); + } + }, [Child]); + + const parent = renderComponent(Parent); + expect(toHtml(parent)).toEqual('before
content
after
'); + }); + it('should re-project content when root.', () => { /**
*/ const GrandChild = createComponent('grand-child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'div'); - { projection(2, 0); } + projectionDef(); + elementStart(0, 'div'); + { projection(1); } elementEnd(); } }); @@ -80,9 +117,9 @@ describe('content projection', () => { /** */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'grand-child'); - { projection(2, 0); } + projectionDef(); + elementStart(0, 'grand-child'); + { projection(1); } elementEnd(); } }, [GrandChild]); @@ -111,9 +148,9 @@ describe('content projection', () => { /**
*/ const Child = createComponent('child', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'div'); - { projection(2, 0); } + projectionDef(); + elementStart(0, 'div'); + { projection(1); } elementEnd(); } }); @@ -149,9 +186,9 @@ describe('content projection', () => { /**
*/ const Child = createComponent('child', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'div'); - { projection(2, 0); } + projectionDef(); + elementStart(0, 'div'); + { projection(1); } elementEnd(); } }); @@ -159,9 +196,9 @@ describe('content projection', () => { /**

*/ const ProjectedComp = createComponent('projected-comp', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'p'); - projection(2, 0); + projectionDef(); + elementStart(0, 'p'); + projection(1); elementEnd(); } }); @@ -198,13 +235,13 @@ describe('content projection', () => { '

Some content
Other content

'); }); - it('should project content with container.', () => { + it('should project containers', () => { /**
*/ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'div'); - { projection(2, 0); } + projectionDef(); + elementStart(0, 'div'); + { projection(1); } elementEnd(); } }); @@ -247,18 +284,20 @@ describe('content projection', () => { 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 into root', () => { + it('should project containers into root', () => { /** */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - projection(1, 0); + projectionDef(); + projection(0); } }); @@ -302,13 +341,13 @@ describe('content projection', () => { expect(toHtml(parent)).toEqual(''); }); - it('should project content with container and if-else.', () => { + it('should project containers with if-else.', () => { /**
*/ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'div'); - { projection(2, 0); } + projectionDef(); + elementStart(0, 'div'); + { projection(1); } elementEnd(); } }); @@ -378,19 +417,19 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'div'); - { container(2); } + projectionDef(); + elementStart(0, 'div'); + { container(1); } elementEnd(); } if (rf & RenderFlags.Update) { - containerRefreshStart(2); + containerRefreshStart(1); { if (!ctx.skipContent) { let rf0 = embeddedViewStart(0); if (rf0 & RenderFlags.Create) { elementStart(0, 'span'); - projection(1, 0); + projection(1); elementEnd(); } embeddedViewEnd(); @@ -400,27 +439,90 @@ describe('content projection', () => { } }); - /** content */ + /** + * + *
text
+ * content + *
+ */ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'child'); { - childCmptInstance = loadDirective(0); - text(1, 'content'); + elementStart(1, 'div'); + { text(2, 'text'); } + elementEnd(); + text(3, 'content'); } elementEnd(); + + // testing + childCmptInstance = loadDirective(0); } }, [Child]); const parent = renderComponent(Parent); - expect(toHtml(parent)).toEqual('
content
'); + expect(toHtml(parent)).toEqual('
text
content
'); childCmptInstance.skipContent = true; detectChanges(parent); expect(toHtml(parent)).toEqual('
'); }); - it('should support projection in embedded views when ng-content is a root node of an embedded view', + it('should support projection into embedded views when no projected nodes', () => { + let childCmptInstance: any; + + /** + *
+ * % if (!skipContent) { + * + * text + * % } + *
+ */ + const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + projectionDef(); + elementStart(0, 'div'); + { container(1); } + elementEnd(); + } + if (rf & RenderFlags.Update) { + containerRefreshStart(1); + { + if (!ctx.skipContent) { + let rf0 = embeddedViewStart(0); + if (rf0 & RenderFlags.Create) { + projection(0); + text(1, 'text'); + } + embeddedViewEnd(); + } + } + containerRefreshEnd(); + } + }); + + /** */ + const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementStart(0, 'child'); + elementEnd(); + + // testing + childCmptInstance = loadDirective(0); + } + }, [Child]); + + const parent = renderComponent(Parent); + expect(toHtml(parent)).toEqual('
text
'); + + childCmptInstance.skipContent = true; + detectChanges(parent); + expect(toHtml(parent)).toEqual('
'); + }); + + it('should support projection into embedded views when ng-content is a root node of an embedded view', () => { let childCmptInstance: any; @@ -433,18 +535,18 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'div'); - { container(2); } + projectionDef(); + elementStart(0, 'div'); + { container(1); } elementEnd(); } if (rf & RenderFlags.Update) { - containerRefreshStart(2); + containerRefreshStart(1); { if (!ctx.skipContent) { let rf0 = embeddedViewStart(0); if (rf0 & RenderFlags.Create) { - projection(0, 0); + projection(0); } embeddedViewEnd(); } @@ -487,22 +589,22 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'div'); + projectionDef(); + elementStart(0, 'div'); { - text(2, 'Before (inside)-'); - container(3); - text(4, '-After (inside)'); + text(1, 'Before (inside)-'); + container(2); + text(3, '-After (inside)'); } elementEnd(); } if (rf & RenderFlags.Update) { - containerRefreshStart(3); + containerRefreshStart(2); { if (!ctx.skipContent) { let rf0 = embeddedViewStart(0); if (rf0 & RenderFlags.Create) { - projection(0, 0); + projection(0); } embeddedViewEnd(); } @@ -567,18 +669,18 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'div'); - { container(2); } + projectionDef(); + elementStart(0, 'div'); + { container(1); } elementEnd(); } if (rf & RenderFlags.Update) { - containerRefreshStart(2); + containerRefreshStart(1); { if (!ctx.skipContent) { let rf0 = embeddedViewStart(0); if (rf0 & RenderFlags.Create) { - projection(0, 0); + projection(0); } embeddedViewEnd(); } @@ -598,22 +700,22 @@ describe('content projection', () => { */ const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'child'); + projectionDef(); + elementStart(0, 'child'); { - text(2, 'Before text'); - container(3); - text(4, '-After text'); + text(1, 'Before text'); + container(2); + text(3, '-After text'); } elementEnd(); } if (rf & RenderFlags.Update) { - containerRefreshStart(3); + containerRefreshStart(2); { if (!ctx.skipContent) { let rf0 = embeddedViewStart(0); if (rf0 & RenderFlags.Create) { - projection(0, 0); + projection(0); } embeddedViewEnd(); } @@ -648,8 +750,7 @@ describe('content projection', () => { .toEqual('
Before text-After text
'); }); - - it('should support projection in embedded views when ng-content is a root node of an embedded view, with other nodes after', + it('should support projection into embedded views when ng-content is a root node of an embedded view, with other nodes after', () => { let childCmptInstance: any; @@ -662,19 +763,19 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'div'); - { container(2); } + projectionDef(); + elementStart(0, 'div'); + { container(1); } elementEnd(); } if (rf & RenderFlags.Update) { - containerRefreshStart(2); + containerRefreshStart(1); { if (!ctx.skipContent) { let rf0 = embeddedViewStart(0); if (rf0 & RenderFlags.Create) { text(0, 'before-'); - projection(1, 0); + projection(1); text(2, '-after'); } embeddedViewEnd(); @@ -732,19 +833,19 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - text(1, 'Before-'); - container(2, IfTemplate, '', [AttributeMarker.SelectOnly, 'ngIf']); - text(3, '-After'); + projectionDef(); + text(0, 'Before-'); + container(1, IfTemplate, '', [AttributeMarker.SelectOnly, 'ngIf']); + text(2, '-After'); } if (rf & RenderFlags.Update) { - elementProperty(2, 'ngIf', bind(ctx.showing)); + elementProperty(1, 'ngIf', bind(ctx.showing)); } function IfTemplate(rf1: RenderFlags, ctx1: any) { if (rf1 & RenderFlags.Create) { - projectionDef(0); - projection(1, 0); + projectionDef(); + projection(0); } } }, [NgIf]); @@ -817,19 +918,19 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - text(1, 'Before-'); - container(2, IfTemplate, '', [AttributeMarker.SelectOnly, 'ngIf']); - text(3, '-After'); + projectionDef(); + text(0, 'Before-'); + container(1, IfTemplate, '', [AttributeMarker.SelectOnly, 'ngIf']); + text(2, '-After'); } if (rf & RenderFlags.Update) { - elementProperty(2, 'ngIf', bind(ctx.showing)); + elementProperty(1, 'ngIf', bind(ctx.showing)); } function IfTemplate(rf1: RenderFlags, ctx1: any) { if (rf1 & RenderFlags.Create) { - projectionDef(0); - projection(1, 0); + projectionDef(); + projection(0); } } }, [NgIf]); @@ -878,12 +979,12 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'div'); - { projection(2, 0); } + projectionDef(); + elementStart(0, 'div'); + { projection(1); } elementEnd(); - elementStart(3, 'span'); - { projection(4, 0); } + elementStart(2, 'span'); + { projection(3); } elementEnd(); } }); @@ -924,19 +1025,19 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - projection(1, 0); - elementStart(2, 'div'); - { container(3); } + projectionDef(); + projection(0); + elementStart(1, 'div'); + { container(2); } elementEnd(); } if (rf & RenderFlags.Update) { - containerRefreshStart(3); + containerRefreshStart(2); { if (ctx.show) { let rf0 = embeddedViewStart(0); if (rf0 & RenderFlags.Create) { - projection(0, 0); + projection(0); } embeddedViewEnd(); } @@ -970,10 +1071,10 @@ describe('content projection', () => { it('should project with multiple instances of a component with projection', () => { const ProjectionComp = createComponent('projection-comp', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - text(1, 'Before'); - projection(2, 0); - text(3, 'After'); + projectionDef(); + text(0, 'Before'); + projection(1); + text(2, 'After'); } }); @@ -990,20 +1091,24 @@ describe('content projection', () => { const AppComp = createComponent('app-comp', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'projection-comp'); - elementStart(1, 'div'); - text(2, 'A'); - elementEnd(); - elementStart(3, 'p'); - text(4, '123'); - elementEnd(); + { + elementStart(1, 'div'); + { text(2, 'A'); } + elementEnd(); + elementStart(3, 'p'); + { text(4, '123'); } + elementEnd(); + } elementEnd(); elementStart(5, 'projection-comp'); - elementStart(6, 'div'); - text(7, 'B'); - elementEnd(); - elementStart(8, 'p'); - text(9, '456'); - elementEnd(); + { + elementStart(6, 'div'); + { text(7, 'B'); } + elementEnd(); + elementStart(8, 'p'); + { text(9, '456'); } + elementEnd(); + } elementEnd(); } }, [ProjectionComp]); @@ -1012,7 +1117,8 @@ describe('content projection', () => { fixture.update(); expect(fixture.html) .toEqual( - 'Before
A

123

After
Before
B

456

After
'); + 'Before
A

123

After
' + + 'Before
B

456

After
'); }); it('should re-project with multiple instances of a component with projection', () => { @@ -1023,10 +1129,10 @@ describe('content projection', () => { */ const ProjectionComp = createComponent('projection-comp', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - text(1, 'Before'); - projection(2, 0); - text(3, 'After'); + projectionDef(); + text(0, 'Before'); + projection(1); + text(2, 'After'); } }); @@ -1043,23 +1149,27 @@ describe('content projection', () => { */ const ProjectionParent = createComponent('parent-comp', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'projection-comp'); - elementStart(2, 'div'); - text(3, 'A'); - elementEnd(); - projection(4, 0); - elementStart(5, 'p'); - text(6, '123'); - elementEnd(); - elementEnd(); - elementStart(7, 'projection-comp'); - elementStart(8, 'div'); - text(9, 'B'); - elementEnd(); - elementStart(10, 'p'); - text(11, '456'); + projectionDef(); + elementStart(0, 'projection-comp'); + { + elementStart(1, 'div'); + { text(2, 'A'); } + elementEnd(); + projection(3, 0); + elementStart(4, 'p'); + { text(5, '123'); } + elementEnd(); + } elementEnd(); + elementStart(6, 'projection-comp'); + { + elementStart(7, 'div'); + { text(8, 'B'); } + elementEnd(); + elementStart(9, 'p'); + { text(10, '456'); } + elementEnd(); + } elementEnd(); } }, [ProjectionComp]); @@ -1075,10 +1185,10 @@ describe('content projection', () => { const AppComp = createComponent('app-comp', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'parent-comp'); - text(1, '**ABC**'); + { text(1, '**ABC**'); } elementEnd(); elementStart(2, 'parent-comp'); - text(3, '**DEF**'); + { text(3, '**DEF**'); } elementEnd(); } }, [ProjectionParent]); @@ -1087,7 +1197,12 @@ describe('content projection', () => { fixture.update(); expect(fixture.html) .toEqual( - 'Before
A
**ABC**

123

After
Before
B

456

After
Before
A
**DEF**

123

After
Before
B

456

After
'); + '' + + 'Before
A
**ABC**

123

After
' + + 'Before
B

456

After
' + + '' + + 'Before
A
**DEF**

123

After
' + + 'Before
B

456

After
'); }); describe('with selectors', () => { @@ -1100,13 +1215,13 @@ describe('content projection', () => { const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef( - 0, [[['span', 'title', 'toFirst']], [['span', 'title', 'toSecond']]], + [[['span', 'title', 'toFirst']], [['span', 'title', 'toSecond']]], ['span[title=toFirst]', 'span[title=toSecond]']); - elementStart(1, 'div', ['id', 'first']); - { projection(2, 0, 1); } + elementStart(0, 'div', ['id', 'first']); + { projection(1, 1); } elementEnd(); - elementStart(3, 'div', ['id', 'second']); - { projection(4, 0, 2); } + elementStart(2, 'div', ['id', 'second']); + { projection(3, 2); } elementEnd(); } }); @@ -1145,8 +1260,8 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0, [[['', 'title', '']]], ['[title]']); - { projection(1, 0, 1); } + projectionDef([[['', 'title', '']]], ['[title]']); + { projection(0, 1); } } }); @@ -1183,17 +1298,16 @@ describe('content projection', () => { const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef( - 0, [ [['span', SelectorFlags.CLASS, 'toFirst']], [['span', SelectorFlags.CLASS, 'toSecond']] ], ['span.toFirst', 'span.toSecond']); - elementStart(1, 'div', ['id', 'first']); - { projection(2, 0, 1); } + elementStart(0, 'div', ['id', 'first']); + { projection(1, 1); } elementEnd(); - elementStart(3, 'div', ['id', 'second']); - { projection(4, 0, 2); } + elementStart(2, 'div', ['id', 'second']); + { projection(3, 2); } elementEnd(); } }); @@ -1233,17 +1347,16 @@ describe('content projection', () => { const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef( - 0, [ [['span', SelectorFlags.CLASS, 'toFirst']], [['span', SelectorFlags.CLASS, 'toSecond']] ], ['span.toFirst', 'span.toSecond']); - elementStart(1, 'div', ['id', 'first']); - { projection(2, 0, 1); } + elementStart(0, 'div', ['id', 'first']); + { projection(1, 1); } elementEnd(); - elementStart(3, 'div', ['id', 'second']); - { projection(4, 0, 2); } + elementStart(2, 'div', ['id', 'second']); + { projection(3, 2); } elementEnd(); } }); @@ -1283,13 +1396,12 @@ describe('content projection', () => { const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef( - 0, [[['span']], [['span', SelectorFlags.CLASS, 'toSecond']]], - ['span', 'span.toSecond']); - elementStart(1, 'div', ['id', 'first']); - { projection(2, 0, 1); } + [[['span']], [['span', SelectorFlags.CLASS, 'toSecond']]], ['span', 'span.toSecond']); + elementStart(0, 'div', ['id', 'first']); + { projection(1, 1); } elementEnd(); - elementStart(3, 'div', ['id', 'second']); - { projection(4, 0, 2); } + elementStart(2, 'div', ['id', 'second']); + { projection(3, 2); } elementEnd(); } }); @@ -1328,12 +1440,12 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0, [[['span', SelectorFlags.CLASS, 'toFirst']]], ['span.toFirst']); - elementStart(1, 'div', ['id', 'first']); - { projection(2, 0, 1); } + projectionDef([[['span', SelectorFlags.CLASS, 'toFirst']]], ['span.toFirst']); + elementStart(0, 'div', ['id', 'first']); + { projection(1, 1); } elementEnd(); - elementStart(3, 'div', ['id', 'second']); - { projection(4, 0); } + elementStart(2, 'div', ['id', 'second']); + { projection(3); } elementEnd(); } }); @@ -1373,12 +1485,12 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0, [[['span', SelectorFlags.CLASS, 'toSecond']]], ['span.toSecond']); - elementStart(1, 'div', ['id', 'first']); - { projection(2, 0); } + projectionDef([[['span', SelectorFlags.CLASS, 'toSecond']]], ['span.toSecond']); + elementStart(0, 'div', ['id', 'first']); + { projection(1); } elementEnd(); - elementStart(3, 'div', ['id', 'second']); - { projection(4, 0, 1); } + elementStart(2, 'div', ['id', 'second']); + { projection(3, 1); } elementEnd(); } }); @@ -1425,11 +1537,11 @@ describe('content projection', () => { */ const GrandChild = createComponent('grand-child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0, [[['span']]], ['span']); - projection(1, 0, 1); - elementStart(2, 'hr'); + projectionDef([[['span']]], ['span']); + projection(0, 1); + elementStart(1, 'hr'); elementEnd(); - projection(3, 0, 0); + projection(2); } }); @@ -1441,12 +1553,12 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'grand-child'); + projectionDef(); + elementStart(0, 'grand-child'); { - projection(2, 0); - elementStart(3, 'span'); - { text(4, 'in child template'); } + projection(1); + elementStart(2, 'span'); + { text(3, 'in child template'); } elementEnd(); } elementEnd(); @@ -1488,12 +1600,12 @@ describe('content projection', () => { const Card = createComponent('card', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef( - 0, [[['', 'card-title', '']], [['', 'card-content', '']]], + [[['', 'card-title', '']], [['', 'card-content', '']]], ['[card-title]', '[card-content]']); - projection(1, 0, 1); - elementStart(2, 'hr'); + projection(0, 1); + elementStart(1, 'hr'); elementEnd(); - projection(3, 0, 2); + projection(2, 2); } }); @@ -1505,13 +1617,13 @@ describe('content projection', () => { */ const CardWithTitle = createComponent('card-with-title', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'card'); + projectionDef(); + elementStart(0, 'card'); { - elementStart(2, 'h1', ['card-title', '']); - { text(3, 'Title'); } + elementStart(1, 'h1', ['card-title', '']); + { text(2, 'Title'); } elementEnd(); - projection(4, 0, 0, ['card-content', '']); + projection(3, 0, ['card-content', '']); } elementEnd(); } @@ -1547,12 +1659,12 @@ describe('content projection', () => { const Card = createComponent('card', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { projectionDef( - 0, [[['', 'card-title', '']], [['', 'card-content', '']]], + [[['', 'card-title', '']], [['', 'card-content', '']]], ['[card-title]', '[card-content]']); - projection(1, 0, 1); - elementStart(2, 'hr'); + projection(0, 1); + elementStart(1, 'hr'); elementEnd(); - projection(3, 0, 2); + projection(2, 2); } }); @@ -1564,13 +1676,13 @@ describe('content projection', () => { */ const CardWithTitle = createComponent('card-with-title', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'card'); + projectionDef(); + elementStart(0, 'card'); { - elementStart(2, 'h1', ['ngProjectAs', '[card-title]']); - { text(3, 'Title'); } + elementStart(1, 'h1', ['ngProjectAs', '[card-title]']); + { text(2, 'Title'); } elementEnd(); - projection(4, 0, 0, ['ngProjectAs', '[card-content]']); + projection(3, 0, ['ngProjectAs', '[card-content]']); } elementEnd(); } @@ -1602,8 +1714,8 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0, [[['div']]], ['div']); - projection(1, 0, 1); + projectionDef([[['div']]], ['div']); + projection(0, 1); } }); @@ -1641,9 +1753,9 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0, [[['div']]], ['div']); - elementStart(1, 'span'); - { projection(2, 0, 1); } + projectionDef([[['div']]], ['div']); + elementStart(0, 'span'); + { projection(1, 1); } elementEnd(); } }); diff --git a/packages/core/test/render3/di_spec.ts b/packages/core/test/render3/di_spec.ts index 540e34dbe4..e65ff383b7 100644 --- a/packages/core/test/render3/di_spec.ts +++ b/packages/core/test/render3/di_spec.ts @@ -947,8 +947,8 @@ describe('di', () => { factory: () => comp = new MyComp(injectChangeDetectorRef()), template: function(rf: RenderFlags, ctx: MyComp) { if (rf & RenderFlags.Create) { - projectionDef(0); - projection(1, 0); + projectionDef(); + projection(0); } } }); diff --git a/packages/core/test/render3/i18n_spec.ts b/packages/core/test/render3/i18n_spec.ts index 98cbf07b3c..b3cc3a957f 100644 --- a/packages/core/test/render3/i18n_spec.ts +++ b/packages/core/test/render3/i18n_spec.ts @@ -971,9 +971,9 @@ describe('Runtime i18n', () => { factory: () => new Child(), template: (rf: RenderFlags, cmp: Child) => { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'p'); - { projection(2, 0); } + projectionDef(); + elementStart(0, 'p'); + { projection(1); } elementEnd(); } } @@ -1063,9 +1063,9 @@ describe('Runtime i18n', () => { factory: () => new Child(), template: (rf: RenderFlags, cmp: Child) => { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'p'); - { projection(2, 0); } + projectionDef(); + elementStart(0, 'p'); + { projection(1); } elementEnd(); } } @@ -1145,9 +1145,9 @@ describe('Runtime i18n', () => { factory: () => new GrandChild(), template: (rf: RenderFlags, cmp: Child) => { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'div'); - { projection(2, 0); } + projectionDef(); + elementStart(0, 'div'); + { projection(1); } elementEnd(); } } @@ -1164,9 +1164,9 @@ describe('Runtime i18n', () => { factory: () => new Child(), template: (rf: RenderFlags, cmp: Child) => { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'grand-child'); - { projection(2, 0); } + projectionDef(); + elementStart(0, 'grand-child'); + { projection(1); } elementEnd(); } } @@ -1224,8 +1224,8 @@ describe('Runtime i18n', () => { factory: () => new Child(), template: (rf: RenderFlags, cmp: Child) => { if (rf & RenderFlags.Create) { - projectionDef(0, [[['span']]], ['span']); - projection(1, 0, 1); + projectionDef([[['span']]], ['span']); + projection(0, 1); } } }); diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index 3d883fe2fe..e334b2a96c 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -502,19 +502,19 @@ describe('render3 integration test', () => { template: function ChildComponentTemplate( rf: RenderFlags, ctx: {beforeTree: Tree, afterTree: Tree}) { if (rf & RenderFlags.Create) { - projectionDef(0); - container(1); - projection(2, 0); - container(3); + projectionDef(); + container(0); + projection(1); + container(2); } - containerRefreshStart(1); + containerRefreshStart(0); { const rf0 = embeddedViewStart(0); { showTree(rf0, {tree: ctx.beforeTree}); } embeddedViewEnd(); } containerRefreshEnd(); - containerRefreshStart(3); + containerRefreshStart(2); { const rf0 = embeddedViewStart(0); { showTree(rf0, {tree: ctx.afterTree}); } diff --git a/packages/core/test/render3/lifecycle_spec.ts b/packages/core/test/render3/lifecycle_spec.ts index bec48f81db..4bb3656fb2 100644 --- a/packages/core/test/render3/lifecycle_spec.ts +++ b/packages/core/test/render3/lifecycle_spec.ts @@ -33,9 +33,9 @@ describe('lifecycles', () => { let Comp = createOnInitComponent('comp', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'div'); - { projection(2, 0); } + projectionDef(); + elementStart(0, 'div'); + { projection(1); } elementEnd(); } }); @@ -500,27 +500,27 @@ describe('lifecycles', () => { let Comp = createAfterContentInitComp('comp', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - projection(1, 0); + projectionDef(); + projection(0); } }); let Parent = createAfterContentInitComp('parent', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'comp'); - { projection(2, 0); } + projectionDef(); + elementStart(0, 'comp'); + { projection(1); } elementEnd(); } if (rf & RenderFlags.Update) { - elementProperty(1, 'val', bind(ctx.val)); + elementProperty(0, 'val', bind(ctx.val)); } }, [Comp]); let ProjectedComp = createAfterContentInitComp('projected', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { - projectionDef(0); - projection(1, 0); + projectionDef(); + projection(0); } }); @@ -893,9 +893,9 @@ describe('lifecycles', () => { let Comp = createAfterViewInitComponent('comp', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'div'); - { projection(2, 0); } + projectionDef(); + elementStart(0, 'div'); + { projection(1); } elementEnd(); } }); @@ -1356,8 +1356,8 @@ describe('lifecycles', () => { let Comp = createOnDestroyComponent('comp', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { - projectionDef(0); - projection(1, 0); + projectionDef(); + projection(0); } }); let Parent = createOnDestroyComponent('parent', getParentTemplate('comp'), [Comp]); @@ -1893,9 +1893,9 @@ describe('lifecycles', () => { const Comp = createOnChangesComponent('comp', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'div'); - { projection(2, 0); } + projectionDef(); + elementStart(0, 'div'); + { projection(1); } elementEnd(); } }); @@ -2444,13 +2444,13 @@ describe('lifecycles', () => { /** */ const Parent = createAllHooksComponent('parent', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { - projectionDef(0); - projection(1, 0); - elementStart(2, 'view'); + projectionDef(); + projection(0); + elementStart(1, 'view'); elementEnd(); } if (rf & RenderFlags.Update) { - elementProperty(2, 'val', bind(ctx.val)); + elementProperty(1, 'val', bind(ctx.val)); } }, [View]); diff --git a/packages/core/test/render3/node_selector_matcher_spec.ts b/packages/core/test/render3/node_selector_matcher_spec.ts index dec13b91d9..f742aacaca 100644 --- a/packages/core/test/render3/node_selector_matcher_spec.ts +++ b/packages/core/test/render3/node_selector_matcher_spec.ts @@ -10,29 +10,16 @@ import {AttributeMarker, TAttributes, TNode, TNodeType} from '../../src/render3/ import {CssSelector, CssSelectorList, NG_PROJECT_AS_ATTR_NAME, SelectorFlags,} from '../../src/render3/interfaces/projection'; import {getProjectAsAttrValue, isNodeMatchingSelectorList, isNodeMatchingSelector} from '../../src/render3/node_selector_matcher'; +import {createTNode} from '@angular/core/src/render3/instructions'; function testLStaticData(tagName: string, attrs: TAttributes | null): TNode { - return { - type: TNodeType.Element, - index: 0, - flags: 0, tagName, attrs, - localNames: null, - initialInputs: undefined, - inputs: undefined, - outputs: undefined, - tViews: null, - next: null, - child: null, - parent: null, - dynamicContainerNode: null, - detached: null, - stylingTemplate: null - }; + return createTNode(TNodeType.Element, 0, tagName, attrs, null, null); } describe('css selector matching', () => { function isMatching(tagName: string, attrs: TAttributes | null, selector: CssSelector): boolean { - return isNodeMatchingSelector(testLStaticData(tagName, attrs), selector); + return isNodeMatchingSelector( + createTNode(TNodeType.Element, 0, tagName, attrs, null, null), selector); } describe('isNodeMatchingSimpleSelector', () => { diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index 75ed3c4502..894b1bc552 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -671,9 +671,9 @@ describe('ViewContainerRef', () => { factory: () => new Child(), template: (rf: RenderFlags, cmp: Child) => { if (rf & RenderFlags.Create) { - projectionDef(0); - elementStart(1, 'div'); - { projection(2, 0); } + projectionDef(); + elementStart(0, 'div'); + { projection(1); } elementEnd(); } } @@ -742,17 +742,17 @@ describe('ViewContainerRef', () => { factory: () => new ChildWithView(), template: (rf: RenderFlags, cmp: ChildWithView) => { if (rf & RenderFlags.Create) { - projectionDef(0); - text(1, 'Before (inside)-'); - container(2); - text(3, 'After (inside)'); + projectionDef(); + text(0, 'Before (inside)-'); + container(1); + text(2, 'After (inside)'); } if (rf & RenderFlags.Update) { - containerRefreshStart(2); + containerRefreshStart(1); if (cmp.show) { let rf0 = embeddedViewStart(0); if (rf0 & RenderFlags.Create) { - projection(0, 0); + projection(0); } embeddedViewEnd(); } @@ -827,12 +827,12 @@ describe('ViewContainerRef', () => { factory: () => new ChildWithSelector(), template: (rf: RenderFlags, cmp: ChildWithSelector) => { if (rf & RenderFlags.Create) { - projectionDef(0, [[['header']]], ['header']); - elementStart(1, 'first'); - { projection(2, 0, 1); } + projectionDef([[['header']]], ['header']); + elementStart(0, 'first'); + { projection(1, 1); } elementEnd(); - elementStart(3, 'second'); - { projection(4, 0); } + elementStart(2, 'second'); + { projection(3); } elementEnd(); } },