diff --git a/packages/core/src/render3/context_discovery.ts b/packages/core/src/render3/context_discovery.ts index 42250da71b1a1..c0c72cf2cb9d9 100644 --- a/packages/core/src/render3/context_discovery.ts +++ b/packages/core/src/render3/context_discovery.ts @@ -8,8 +8,10 @@ import './ng_dev_mode'; import {assertEqual} from './assert'; +import {ACTIVE_INDEX, HOST_NATIVE, LContainer} from './interfaces/container'; import {LElementNode, TNode, TNodeFlags} from './interfaces/node'; import {RElement} from './interfaces/renderer'; +import {StylingContext, StylingIndex} from './interfaces/styling'; import {CONTEXT, HEADER_OFFSET, LViewData, TVIEW} from './interfaces/view'; /** @@ -390,6 +392,26 @@ function getDirectiveEndIndex(tNode: TNode, startIndex: number): number { return count ? (startIndex + count) : -1; } -export function readElementValue(value: LElementNode | any[]): LElementNode { - return (Array.isArray(value) ? (value as any as any[])[0] : value) as LElementNode; +/** + * Takes the value of a slot in `LViewData` and returns the element node. + * + * Normally, element nodes are stored flat, but if the node has styles/classes on it, + * it might be wrapped in a styling context. Or if that node has a directive that injects + * ViewContainerRef, it may be wrapped in an LContainer. + * + * @param value The initial value in `LViewData` + */ +export function readElementValue(value: LElementNode | StylingContext | LContainer): LElementNode { + if (Array.isArray(value)) { + if (typeof value[ACTIVE_INDEX] === 'number') { + // This is an LContainer. It may also have a styling context. + value = value[HOST_NATIVE] as LElementNode | StylingContext; + return Array.isArray(value) ? value[StylingIndex.ElementPosition] ! : value; + } else { + // This is a StylingContext, which stores the element node at 0. + return value[StylingIndex.ElementPosition] as LElementNode; + } + } else { + return value; // Regular LNode is stored here + } } diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts index 1705d55c4a326..1fb6d688a5719 100644 --- a/packages/core/src/render3/i18n.ts +++ b/packages/core/src/render3/i18n.ts @@ -8,11 +8,13 @@ import {assertEqual, assertLessThan} from './assert'; import {NO_CHANGE, _getViewData, adjustBlueprintForNewNode, bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4, createNodeAtIndex, getRenderer, getTNode, load, loadElement, resetComponentState} from './instructions'; -import {RENDER_PARENT} from './interfaces/container'; +import {LContainer, NATIVE, RENDER_PARENT} from './interfaces/container'; import {LContainerNode, LNode, TElementNode, TNode, TNodeType} from './interfaces/node'; +import {StylingContext} from './interfaces/styling'; import {BINDING_INDEX, HEADER_OFFSET, HOST_NODE, TVIEW} from './interfaces/view'; import {appendChild, createTextNode, removeChild} from './node_manipulation'; -import {stringify} from './util'; +import {getLNode, isLContainer, stringify} from './util'; + /** * A list of flags to encode the i18n instructions used to translate the template. @@ -245,9 +247,7 @@ function generateMappingInstructions( return partIndex; } -// TODO: Remove LNode arg when we remove dynamicContainerNode -function appendI18nNode( - node: LNode, tNode: TNode, parentTNode: TNode, previousTNode: TNode): TNode { +function appendI18nNode(tNode: TNode, parentTNode: TNode, previousTNode: TNode): TNode { if (ngDevMode) { ngDevMode.rendererMoveNode++; } @@ -272,11 +272,13 @@ function appendI18nNode( } } - appendChild(node.native, tNode, viewData); + const native = getLNode(tNode, viewData).native; + appendChild(native, tNode, viewData); - // Template containers also have a comment node for the `ViewContainerRef` that should be moved - if (tNode.type === TNodeType.Container && node.dynamicLContainerNode) { - appendChild(node.dynamicLContainerNode.native, tNode, viewData); + const slotValue = viewData[tNode.index]; + if (tNode.type !== TNodeType.Container && isLContainer(slotValue)) { + // Nodes that inject ViewContainerRef also have a comment node that should be moved + appendChild(slotValue[NATIVE], tNode, viewData); } return tNode; @@ -327,20 +329,16 @@ export function i18nApply(startIndex: number, instructions: I18nInstruction[]): const instruction = instructions[i] as number; switch (instruction & I18nInstructions.InstructionMask) { case I18nInstructions.Element: - const elementIndex = instruction & I18nInstructions.IndexMask; - const element: LNode = load(elementIndex); - const elementTNode = getTNode(elementIndex); - localPreviousTNode = - appendI18nNode(element, elementTNode, localParentTNode, localPreviousTNode); + const elementTNode = getTNode(instruction & I18nInstructions.IndexMask); + localPreviousTNode = appendI18nNode(elementTNode, localParentTNode, localPreviousTNode); localParentTNode = elementTNode; break; case I18nInstructions.Expression: case I18nInstructions.TemplateRoot: case I18nInstructions.Any: const nodeIndex = instruction & I18nInstructions.IndexMask; - const node: LNode = load(nodeIndex); localPreviousTNode = - appendI18nNode(node, getTNode(nodeIndex), localParentTNode, localPreviousTNode); + appendI18nNode(getTNode(nodeIndex), localParentTNode, localPreviousTNode); break; case I18nInstructions.Text: if (ngDevMode) { @@ -352,11 +350,9 @@ export function i18nApply(startIndex: number, instructions: I18nInstruction[]): // Create text node at the current end of viewData. Must subtract header offset because // createNodeAtIndex takes a raw index (not adjusted by header offset). adjustBlueprintForNewNode(viewData); - const lastNodeIndex = viewData.length - 1 - HEADER_OFFSET; - const textTNode = - createNodeAtIndex(lastNodeIndex, TNodeType.Element, textRNode, null, null); - localPreviousTNode = appendI18nNode( - loadElement(lastNodeIndex), textTNode, localParentTNode, localPreviousTNode); + const textTNode = createNodeAtIndex( + viewData.length - 1 - HEADER_OFFSET, TNodeType.Element, textRNode, null, null); + localPreviousTNode = appendI18nNode(textTNode, localParentTNode, localPreviousTNode); resetComponentState(); break; case I18nInstructions.CloseNode: @@ -368,15 +364,18 @@ export function i18nApply(startIndex: number, instructions: I18nInstruction[]): ngDevMode.rendererRemoveNode++; } const removeIndex = instruction & I18nInstructions.IndexMask; - const removedNode: LNode|LContainerNode = load(removeIndex); + const removedNode: LNode|LContainerNode = loadElement(removeIndex); const removedTNode = getTNode(removeIndex); removeChild(removedTNode, removedNode.native || null, viewData); - // For template containers we also need to remove their `ViewContainerRef` from the DOM - if (removedTNode.type === TNodeType.Container && removedNode.dynamicLContainerNode) { - removeChild(removedTNode, removedNode.dynamicLContainerNode.native || null, viewData); + const slotValue = load(removeIndex) as LNode | LContainer | StylingContext; + if (isLContainer(slotValue)) { + const lContainer = slotValue as LContainer; + if (removedTNode.type !== TNodeType.Container) { + removeChild(removedTNode, lContainer[NATIVE] || null, viewData); + } removedTNode.detached = true; - removedNode.dynamicLContainerNode.data[RENDER_PARENT] = null; + lContainer[RENDER_PARENT] = null; } break; } diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 27ceed20981a1..da0436a01afd2 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -16,7 +16,7 @@ import {assertDefined, assertEqual, assertLessThan, assertNotEqual} from './asse import {attachPatchData, getLElementFromComponent, readElementValue, readPatchedLViewData} from './context_discovery'; import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors'; import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks'; -import {ACTIVE_INDEX, LContainer, RENDER_PARENT, VIEWS} from './interfaces/container'; +import {ACTIVE_INDEX, HOST_NATIVE, LContainer, RENDER_PARENT, VIEWS} from './interfaces/container'; import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, InitialStylingFlags, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; import {INJECTOR_SIZE} from './interfaces/injector'; import {AttributeMarker, InitialInputData, InitialInputs, LContainerNode, LElementContainerNode, LElementNode, LNode, LProjectionNode, LTextNode, LViewNode, LocalRefExtractor, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeType, TProjectionNode, TViewNode} from './interfaces/node'; @@ -29,7 +29,7 @@ import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {appendChild, appendProjectedNode, createTextNode, findComponentView, getHostElementNode, getLViewChild, getRenderParent, insertView, removeView} from './node_manipulation'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; import {allocStylingContext, createStylingContextTemplate, renderStyling as renderElementStyles, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings'; -import {assertDataInRangeInternal, getLNode, getRootView, isContentQueryHost, isDifferent, loadElementInternal, loadInternal, stringify} from './util'; +import {assertDataInRangeInternal, getLNode, getRootView, isContentQueryHost, isDifferent, isLContainer, loadElementInternal, loadInternal, stringify} from './util'; /** @@ -430,7 +430,7 @@ export function createLViewData( export function createLNodeObject( type: TNodeType, native: RText | RElement | RComment | null, state: any): LElementNode& LTextNode&LViewNode&LContainerNode&LProjectionNode { - return {native: native as any, data: state, dynamicLContainerNode: null}; + return {native: native as any, data: state}; } /** @@ -453,7 +453,7 @@ export function createNodeAtIndex( lViewData: LViewData): TViewNode; export function createNodeAtIndex( index: number, type: TNodeType.Container, native: RComment, name: string | null, - attrs: TAttributes | null, lContainer: LContainer): TContainerNode; + attrs: TAttributes | null, data: null): TContainerNode; export function createNodeAtIndex( index: number, type: TNodeType.Projection, native: null, name: null, attrs: TAttributes | null, lProjection: null): TProjectionNode; @@ -935,8 +935,10 @@ function cacheMatchingDirectivesForNode( function generateExpandoBlock(tNode: TNode, matches: CurrentMatchesList | null): void { const directiveCount = matches ? matches.length / 2 : 0; const elementIndex = -(tNode.index - HEADER_OFFSET); - (tView.expandoInstructions || (tView.expandoInstructions = [ - ])).push(elementIndex, directiveCount); + if (directiveCount > 0) { + (tView.expandoInstructions || (tView.expandoInstructions = [ + ])).push(elementIndex, directiveCount); + } } /** @@ -1418,7 +1420,7 @@ export function elementAttribute( export function elementProperty( index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn): void { if (value === NO_CHANGE) return; - const node = loadElement(index) as LElementNode; + const node = loadElement(index) as LElementNode | LContainerNode | LElementContainerNode; const tNode = getTNode(index); // if tNode.inputs is undefined, a listener has created outputs, but inputs haven't // yet been checked @@ -1431,12 +1433,12 @@ export function elementProperty( let dataValue: PropertyAliasValue|undefined; if (inputData && (dataValue = inputData[propName])) { setInputsForProperty(dataValue, value); - markDirtyIfOnPush(node); - } else { + if (tNode.type === TNodeType.Element) markDirtyIfOnPush(node as LElementNode); + } else if (tNode.type === TNodeType.Element) { // It is assumed that the sanitizer is only added when the compiler determines that the property // is risky, so sanitization can be done without further checks. value = sanitizer != null ? (sanitizer(value) as any) : value; - const native = node.native; + const native = node.native as RElement; ngDevMode && ngDevMode.rendererSetProperty++; isProceduralRenderer(renderer) ? renderer.setProperty(native, propName, value) : (native.setProperty ? native.setProperty(propName, value) : @@ -1639,16 +1641,22 @@ export function elementStyling( * @param index Index of the style allocation. See: `elementStyling`. */ function getStylingContext(index: number): StylingContext { - let stylingContext = load(index); - if (!Array.isArray(stylingContext)) { - const lElement = stylingContext as any as LElementNode; - const tNode = getTNode(index); - ngDevMode && - assertDefined(tNode.stylingTemplate, 'getStylingContext() called before elementStyling()'); - stylingContext = viewData[index + HEADER_OFFSET] = - allocStylingContext(lElement, tNode.stylingTemplate !); + let slotValue = viewData[index + HEADER_OFFSET]; + + if (isLContainer(slotValue)) { + const lContainer = slotValue; + slotValue = lContainer[HOST_NATIVE]; + if (!Array.isArray(slotValue)) { + return lContainer[HOST_NATIVE] = + allocStylingContext(slotValue, getTNode(index).stylingTemplate !); + } + } else if (!Array.isArray(slotValue)) { + // This is a regular ElementNode + return viewData[index + HEADER_OFFSET] = + allocStylingContext(slotValue, getTNode(index).stylingTemplate !); } - return stylingContext; + + return slotValue as StylingContext; } /** @@ -1969,19 +1977,26 @@ function generateInitialInputs( /** * Creates a LContainer, either from a container instruction, or for a ViewContainerRef. * + * @param hostLNode The host node for the LContainer + * @param hostTNode The host TNode for the LContainer * @param currentView The parent view of the LContainer + * @param native The native comment element * @param isForViewContainerRef Optional a flag indicating the ViewContainerRef case * @returns LContainer */ export function createLContainer( - currentView: LViewData, isForViewContainerRef?: boolean): LContainer { + hostLNode: LElementNode | LContainerNode | LElementContainerNode, + hostTNode: TElementNode | TContainerNode | TElementContainerNode, currentView: LViewData, + native: RComment, isForViewContainerRef?: boolean): LContainer { return [ - isForViewContainerRef ? null : 0, // active index - currentView, // parent - null, // next - null, // queries - [], // views - null // renderParent, set after node creation + isForViewContainerRef ? -1 : 0, // active index + currentView, // parent + null, // next + null, // queries + hostLNode, // host native + native, // native + [], // views + getRenderParent(hostTNode, currentView) // renderParent ]; } @@ -2042,12 +2057,13 @@ function containerInternal( viewData[BINDING_INDEX], tView.bindingStartIndex, 'container nodes should be created before any bindings'); - const lContainer = createLContainer(viewData); - ngDevMode && ngDevMode.rendererCreateComment++; + const adjustedIndex = index + HEADER_OFFSET; const comment = renderer.createComment(ngDevMode ? 'container' : ''); - const tNode = createNodeAtIndex(index, TNodeType.Container, comment, tagName, attrs, lContainer); + ngDevMode && ngDevMode.rendererCreateComment++; + const tNode = createNodeAtIndex(index, TNodeType.Container, comment, tagName, attrs, null); + const lContainer = viewData[adjustedIndex] = + createLContainer(viewData[adjustedIndex], tNode, viewData, comment); - lContainer[RENDER_PARENT] = getRenderParent(tNode, viewData); appendChild(comment, tNode, viewData); // Containers are added to the current view tree instead of their embedded views @@ -2073,8 +2089,8 @@ export function containerRefreshStart(index: number): void { ngDevMode && assertNodeType(previousOrParentTNode, TNodeType.Container); isParent = true; - // Inline containers cannot have style bindings, so we can read the value directly - (viewData[previousOrParentTNode.index] as LContainerNode).data[ACTIVE_INDEX] = 0; + + viewData[index + HEADER_OFFSET][ACTIVE_INDEX] = 0; if (!checkNoChangesMode) { // We need to execute init hooks here so ngOnInit hooks are called in top level views @@ -2099,8 +2115,7 @@ export function containerRefreshEnd(): void { ngDevMode && assertNodeType(previousOrParentTNode, TNodeType.Container); - // Inline containers cannot have style bindings, so we can read the value directly - const lContainer = viewData[previousOrParentTNode.index].data; + const lContainer = viewData[previousOrParentTNode.index]; const nextIndex = lContainer[ACTIVE_INDEX]; // remove extra views at the end of the container @@ -2118,7 +2133,7 @@ function refreshDynamicEmbeddedViews(lViewData: LViewData) { // Note: current can be an LViewData or an LContainer instance, but here we are only interested // in LContainer. We can tell it's an LContainer because its length is less than the LViewData // header. - if (current.length < HEADER_OFFSET && current[ACTIVE_INDEX] === null) { + if (current.length < HEADER_OFFSET && current[ACTIVE_INDEX] === -1) { const container = current as LContainer; for (let i = 0; i < container[VIEWS].length; i++) { const dynamicViewData = container[VIEWS][i]; @@ -2175,12 +2190,10 @@ export function embeddedViewStart(viewBlockId: number, consts: number, vars: num const containerTNode = previousOrParentTNode.type === TNodeType.View ? previousOrParentTNode.parent ! : previousOrParentTNode; - // Inline containers cannot have style bindings, so we can read the value directly - const container = viewData[containerTNode.index] as LContainerNode; + const lContainer = viewData[containerTNode.index] as LContainer; const currentView = viewData; ngDevMode && assertNodeType(containerTNode, TNodeType.Container); - const lContainer = container.data; let viewToRender = scanForView( lContainer, containerTNode as TContainerNode, lContainer[ACTIVE_INDEX] !, viewBlockId); @@ -2201,7 +2214,7 @@ export function embeddedViewStart(viewBlockId: number, consts: number, vars: num createNodeAtIndex(viewBlockId, TNodeType.View, null, null, null, viewToRender); enterView(viewToRender, viewToRender[TVIEW].node); } - if (container) { + if (lContainer) { if (creationMode) { // it is a new view, insert it into collection of views for a given container insertView(viewToRender, lContainer, currentView, lContainer[ACTIVE_INDEX] !, -1); @@ -2409,12 +2422,10 @@ export function projection(nodeIndex: number, selectorIndex: number = 0, attrs?: continue; } } else { - const lNode = projectedView[nodeToProject.index] as LTextNode | LElementNode | LContainerNode; // This flag must be set now or we won't know that this node is projected // if the nodes are inserted into a container later. nodeToProject.flags |= TNodeFlags.isProjected; - - appendProjectedNode(lNode, nodeToProject, tProjectionNode, viewData, projectedView); + appendProjectedNode(nodeToProject, tProjectionNode, viewData, projectedView); } // If we are finished with a list of re-projected nodes, we need to get diff --git a/packages/core/src/render3/interfaces/container.ts b/packages/core/src/render3/interfaces/container.ts index e4559bb9a0dfb..3690471c348a0 100644 --- a/packages/core/src/render3/interfaces/container.ts +++ b/packages/core/src/render3/interfaces/container.ts @@ -6,8 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {LElementNode} from './node'; +import {LContainerNode, LElementContainerNode, LElementNode} from './node'; import {LQueries} from './query'; +import {RComment} from './renderer'; +import {StylingContext} from './styling'; import {LViewData, NEXT, PARENT, QUERIES} from './view'; /** @@ -18,8 +20,10 @@ import {LViewData, NEXT, PARENT, QUERIES} from './view'; export const ACTIVE_INDEX = 0; // PARENT, NEXT, and QUERIES are indices 1, 2, and 3. // As we already have these constants in LViewData, we don't need to re-create them. -export const VIEWS = 4; -export const RENDER_PARENT = 5; +export const HOST_NATIVE = 4; +export const NATIVE = 5; +export const VIEWS = 6; +export const RENDER_PARENT = 7; /** * The state associated with an LContainerNode. @@ -37,7 +41,7 @@ export interface LContainer extends Array { * it is set to null to identify this scenario, as indices are "absolute" in that case, * i.e. provided directly by the user of the ViewContainerRef API. */ - [ACTIVE_INDEX]: number|null; + [ACTIVE_INDEX]: number; /** * Access to the parent view is necessary so we can propagate back @@ -57,6 +61,13 @@ export interface LContainer extends Array { */ [QUERIES]: LQueries|null; + /** The host node of this LContainer. */ + // TODO: Should contain just the native element once LNode is removed. + [HOST_NATIVE]: LElementNode|LContainerNode|LElementContainerNode|StylingContext; + + /** The comment element that serves as an anchor for this LContainer. */ + [NATIVE]: RComment; + /** * A list of the container's currently active child views. Views will be inserted * here as they are added and spliced from here when they are removed. We need diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index e004561e28f13..49a0c94a0930a 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -71,18 +71,12 @@ export interface LNode { readonly native: RComment|RElement|RText|null; /** - * If regular LElementNode, LTextNode, and LProjectionNode then `data` will be null. + * If regular LElementNode, LTextNode, LContainerNode, and LProjectionNode then `data` will be + * null. * If LElementNode with component, then `data` contains LViewData. * If LViewNode, then `data` contains the LViewData. - * If LContainerNode, then `data` contains LContainer. */ - readonly data: LViewData|LContainer|null; - - /** - * A pointer to an LContainerNode created by directives requesting ViewContainerRef - */ - // TODO(kara): Remove when removing LNodes - dynamicLContainerNode: LContainerNode|null; + readonly data: LViewData|null; } @@ -107,14 +101,12 @@ export interface LTextNode extends LNode { /** The text node associated with this node. */ native: RText; readonly data: null; - dynamicLContainerNode: null; } /** Abstract node which contains root nodes of a view. */ export interface LViewNode extends LNode { readonly native: null; readonly data: LViewData; - dynamicLContainerNode: null; } /** Abstract node container which contains other views. */ @@ -127,14 +119,13 @@ export interface LContainerNode extends LNode { * until the parent view is processed. */ native: RComment; - readonly data: LContainer; + readonly data: null; } export interface LProjectionNode extends LNode { readonly native: null; readonly data: null; - dynamicLContainerNode: null; } /** diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 4484175870084..5bde9d2dc02b4 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -9,13 +9,14 @@ import {assertDefined} from './assert'; import {attachPatchData, readElementValue} from './context_discovery'; import {callHooks} from './hooks'; -import {LContainer, RENDER_PARENT, VIEWS, unusedValueExportToPlacateAjd as unused1} from './interfaces/container'; +import {HOST_NATIVE, LContainer, NATIVE, RENDER_PARENT, VIEWS, unusedValueExportToPlacateAjd as unused1} from './interfaces/container'; import {LContainerNode, LElementContainerNode, LElementNode, LTextNode, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeType, TViewNode, 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 {StylingIndex} from './interfaces/styling'; import {CLEANUP, CONTAINER_INDEX, FLAGS, HEADER_OFFSET, HOST_NODE, HookData, LViewData, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, TVIEW, unusedValueExportToPlacateAjd as unused5} from './interfaces/view'; import {assertNodeType} from './node_assert'; -import {getLNode, stringify} from './util'; +import {getLNode, isLContainer, stringify} from './util'; const unusedValueToPlacateAjd = unused1 + unused2 + unused3 + unused4 + unused5; @@ -37,17 +38,15 @@ export function getHostElementNode(currentView: LViewData): LElementNode|null { null; } -export function getContainerNode(tNode: TNode, embeddedView: LViewData): LContainerNode|null { +export function getLContainer(tNode: TViewNode, embeddedView: LViewData): LContainer|null { if (tNode.index === -1) { // This is a dynamically created view inside a dynamic container. // If the host index is -1, the view has not yet been inserted, so it has no parent. const containerHostIndex = embeddedView[CONTAINER_INDEX]; - return containerHostIndex > -1 ? - embeddedView[PARENT] ![containerHostIndex].dynamicLContainerNode : - null; + return containerHostIndex > -1 ? embeddedView[PARENT] ![containerHostIndex] : null; } else { // This is a inline view node (e.g. embeddedViewStart) - return getParentLNode(tNode, embeddedView[PARENT] !) as LContainerNode; + return embeddedView[PARENT] ![tNode.parent !.index] as LContainer; } } @@ -57,8 +56,8 @@ export function getContainerNode(tNode: TNode, embeddedView: LViewData): LContai * Might be null if a view is not yet attached to any container. */ export function getContainerRenderParent(tViewNode: TViewNode, view: LViewData): LElementNode|null { - const container = getContainerNode(tViewNode, view); - return container ? container.data[RENDER_PARENT] : null; + const container = getLContainer(tViewNode, view); + return container ? container[RENDER_PARENT] : null; } const enum WalkTNodeTreeAction { @@ -107,29 +106,24 @@ function walkTNodeTree( if (tNode.type === TNodeType.Element) { const elementNode = getLNode(tNode, currentView); executeNodeAction(action, renderer, parent, elementNode.native !, beforeNode); - if (elementNode.dynamicLContainerNode) { - executeNodeAction( - action, renderer, parent, elementNode.dynamicLContainerNode.native !, beforeNode); + const nodeOrContainer = currentView[tNode.index]; + if (isLContainer(nodeOrContainer)) { + // This element has an LContainer, and its comment needs to be handled + executeNodeAction(action, renderer, parent, nodeOrContainer[NATIVE], beforeNode); } } else if (tNode.type === TNodeType.Container) { - const lContainerNode: LContainerNode = currentView ![tNode.index] as LContainerNode; - executeNodeAction(action, renderer, parent, lContainerNode.native !, beforeNode); - const childContainerData: LContainer = lContainerNode.dynamicLContainerNode ? - lContainerNode.dynamicLContainerNode.data : - lContainerNode.data; - if (renderParentNode) { - childContainerData[RENDER_PARENT] = renderParentNode; - } + const lContainer = currentView ![tNode.index] as LContainer; + executeNodeAction(action, renderer, parent, lContainer[NATIVE], beforeNode); + + if (renderParentNode) lContainer[RENDER_PARENT] = renderParentNode; - if (childContainerData[VIEWS].length) { - currentView = childContainerData[VIEWS][0]; + if (lContainer[VIEWS].length) { + currentView = lContainer[VIEWS][0]; nextTNode = currentView[TVIEW].node; // When the walker enters a container, then the beforeNode has to become the local native // comment node. - beforeNode = lContainerNode.dynamicLContainerNode ? - lContainerNode.dynamicLContainerNode.native : - lContainerNode.native; + beforeNode = lContainer[NATIVE]; } } else if (tNode.type === TNodeType.Projection) { const componentView = findComponentView(currentView !); @@ -174,7 +168,7 @@ function walkTNodeTree( // When exiting a container, the beforeNode must be restored to the previous value if (tNode.type === TNodeType.Container) { currentView = currentView[PARENT] !; - beforeNode = currentView[tNode.index].native; + beforeNode = currentView[tNode.index][NATIVE]; } if (tNode.type === TNodeType.View && currentView[NEXT]) { @@ -402,11 +396,14 @@ export function removeView( /** Gets the child of the given LViewData */ export function getLViewChild(viewData: LViewData): LViewData|LContainer|null { - if (viewData[TVIEW].childIndex === -1) return null; + const childIndex = viewData[TVIEW].childIndex; + if (childIndex === -1) return null; - const hostNode: LElementNode|LContainerNode = viewData[viewData[TVIEW].childIndex]; + const value: LElementNode|LContainerNode|LContainer = viewData[childIndex]; - return hostNode.data ? hostNode.data : (hostNode.dynamicLContainerNode as LContainerNode).data; + // If it's an array, it's an LContainer. Otherwise, it's a component node, so LViewData + // is stored in data. + return Array.isArray(value) ? value : value.data; } /** @@ -444,7 +441,7 @@ export function getParentState(state: LViewData | LContainer, rootView: LViewDat tNode.type === TNodeType.View) { // if it's an embedded view, the state needs to go up to the container, in case the // container has a next - return getContainerNode(tNode, state as LViewData) !.data as any; + return getLContainer(tNode as TViewNode, state as LViewData) as LContainer; } else { // otherwise, use parent view for containers or component views return state[PARENT] === rootView ? null : state[PARENT]; @@ -457,7 +454,7 @@ export function getParentState(state: LViewData | LContainer, rootView: LViewDat * @param view The LViewData to clean up */ function cleanUpView(viewOrContainer: LViewData | LContainer): void { - if ((viewOrContainer as LViewData)[TVIEW]) { + if ((viewOrContainer as LViewData).length >= HEADER_OFFSET) { const view = viewOrContainer as LViewData; removeListeners(view); executeOnDestroys(view); @@ -552,8 +549,8 @@ function canInsertNativeChildOfElement(tNode: TNode): boolean { */ function canInsertNativeChildOfView(viewTNode: TViewNode, view: LViewData): boolean { // Because we are inserting into a `View` the `View` may be disconnected. - const container = getContainerNode(viewTNode, view) !; - if (container == null || container.data[RENDER_PARENT] == null) { + const container = getLContainer(viewTNode, view) !; + if (container == null || container[RENDER_PARENT] == null) { // The `View` is not inserted into a `Container` or the parent `Container` // itself is disconnected. So we have to delay. return false; @@ -634,12 +631,13 @@ export function appendChild( const parentTNode: TNode = childTNode.parent || currentView[HOST_NODE] !; if (parentTNode.type === TNodeType.View) { - const container = getContainerNode(parentTNode, currentView) as LContainerNode; - const renderParent = container.data[RENDER_PARENT]; - const views = container.data[VIEWS]; + const lContainer = getLContainer(parentTNode as TViewNode, currentView) as LContainer; + const renderParent = lContainer[RENDER_PARENT]; + const views = lContainer[VIEWS]; const index = views.indexOf(currentView); nativeInsertBefore( - renderer, renderParent !.native, childEl, getBeforeNodeForView(index, views, container)); + renderer, renderParent !.native, childEl, + getBeforeNodeForView(index, views, lContainer[NATIVE])); } else if (parentTNode.type === TNodeType.ElementContainer) { let elementContainer = getHighestElementContainer(childTNode); let node: LElementNode = getRenderParent(elementContainer, currentView) !; @@ -666,13 +664,13 @@ function getHighestElementContainer(ngContainer: TNode): TNode { return ngContainer; } -export function getBeforeNodeForView(index: number, views: LViewData[], container: LContainerNode) { +export function getBeforeNodeForView(index: number, views: LViewData[], containerNative: RComment) { if (index + 1 < views.length) { const view = views[index + 1] as LViewData; const viewTNode = view[HOST_NODE] as TViewNode; - return viewTNode.child ? getLNode(viewTNode.child, view).native : container.native; + return viewTNode.child ? getLNode(viewTNode.child, view).native : containerNative; } else { - return container.native; + return containerNative; } } @@ -700,49 +698,48 @@ export function removeChild(tNode: TNode, child: RNode | null, currentView: LVie * Appends a projected node to the DOM, or in the case of a projected container, * appends the nodes from all of the container's active views to the DOM. * - * @param projectedLNode The node to process - * @param parentNode The last parent element to be processed - * @param tProjectionNode + * @param projectedTNode The TNode to be projected + * @param tProjectionNode The projection (ng-content) TNode * @param currentView Current LView - * @param projectionView Projection view + * @param projectionView Projection view (view above current) */ export function appendProjectedNode( - projectedLNode: LElementNode | LElementContainerNode | LTextNode | LContainerNode, projectedTNode: TNode, tProjectionNode: TNode, currentView: LViewData, projectionView: LViewData): void { - appendChild(projectedLNode.native, tProjectionNode, currentView); + const native = getLNode(projectedTNode, projectionView).native; + appendChild(native, tProjectionNode, currentView); // the projected contents are processed while in the shadow view (which is the currentView) // therefore we need to extract the view where the host element lives since it's the // logical container of the content projected views - attachPatchData(projectedLNode.native, projectionView); + attachPatchData(native, projectionView); const renderParent = getRenderParent(tProjectionNode, currentView); + const nodeOrContainer = projectionView[projectedTNode.index]; if (projectedTNode.type === TNodeType.Container) { // The node we are adding is a container and we are adding it to an element which // is not a component (no more re-projection). // Alternatively a container is projected at the root of a component's template // and can't be re-projected (as not content of any component). // Assign the final projection location in those cases. - const lContainer = (projectedLNode as LContainerNode).data; - lContainer[RENDER_PARENT] = renderParent; - const views = lContainer[VIEWS]; + nodeOrContainer[RENDER_PARENT] = renderParent; + const views = nodeOrContainer[VIEWS]; for (let i = 0; i < views.length; i++) { - addRemoveViewFromContainer(views[i], true, projectedLNode.native); + addRemoveViewFromContainer(views[i], true, nodeOrContainer[NATIVE]); } - } else if (projectedTNode.type === TNodeType.ElementContainer) { - let ngContainerChildTNode: TNode|null = projectedTNode.child as TNode; - while (ngContainerChildTNode) { - let ngContainerChild = getLNode(ngContainerChildTNode, projectionView); - appendProjectedNode( - ngContainerChild as LElementNode | LElementContainerNode | LTextNode | LContainerNode, - ngContainerChildTNode, tProjectionNode, currentView, projectionView); - ngContainerChildTNode = ngContainerChildTNode.next; + } else { + if (projectedTNode.type === TNodeType.ElementContainer) { + let ngContainerChildTNode: TNode|null = projectedTNode.child as TNode; + while (ngContainerChildTNode) { + appendProjectedNode(ngContainerChildTNode, tProjectionNode, currentView, projectionView); + ngContainerChildTNode = ngContainerChildTNode.next; + } + } + + if (isLContainer(nodeOrContainer)) { + nodeOrContainer[RENDER_PARENT] = renderParent; + appendChild(nodeOrContainer[NATIVE], tProjectionNode, currentView); } - } - if (projectedLNode.dynamicLContainerNode) { - projectedLNode.dynamicLContainerNode.data[RENDER_PARENT] = renderParent; - appendChild(projectedLNode.dynamicLContainerNode.native, tProjectionNode, currentView); } } diff --git a/packages/core/src/render3/util.ts b/packages/core/src/render3/util.ts index 1036ed829cd11..fa90a0b0a31e9 100644 --- a/packages/core/src/render3/util.ts +++ b/packages/core/src/render3/util.ts @@ -10,11 +10,12 @@ import {devModeEqual} from '../change_detection/change_detection_util'; import {assertDefined, assertLessThan} from './assert'; import {readElementValue, readPatchedLViewData} from './context_discovery'; -import {LContainerNode, LElementContainerNode, LElementNode, TNode, TNodeFlags} from './interfaces/node'; +import {ACTIVE_INDEX, LContainer} from './interfaces/container'; +import {LContainerNode, LElementContainerNode, LElementNode, LNode, TNode, TNodeFlags} from './interfaces/node'; +import {StylingContext} from './interfaces/styling'; import {CONTEXT, FLAGS, HEADER_OFFSET, LViewData, LViewFlags, PARENT, RootContext, TData, TVIEW} from './interfaces/view'; - /** * Returns whether the values are different from a change detection stand point. * @@ -103,6 +104,12 @@ export function isContentQueryHost(tNode: TNode): boolean { export function isComponent(tNode: TNode): boolean { return (tNode.flags & TNodeFlags.isComponent) === TNodeFlags.isComponent; } + +export function isLContainer(value: LNode | LContainer | StylingContext): boolean { + // Styling contexts are also arrays, but their first index contains an element node + return Array.isArray(value) && typeof value[ACTIVE_INDEX] === 'number'; +} + /** * Retrieve the root view from any component by walking the parent `LViewData` until * reaching the root `LViewData`. diff --git a/packages/core/src/render3/view_engine_compatibility.ts b/packages/core/src/render3/view_engine_compatibility.ts index b24f076c1d69d..c772dcb28c60f 100644 --- a/packages/core/src/render3/view_engine_compatibility.ts +++ b/packages/core/src/render3/view_engine_compatibility.ts @@ -18,17 +18,17 @@ import {Renderer2} from '../render/api'; import {assertDefined, assertGreaterThan, assertLessThan} from './assert'; import {NodeInjector, getParentInjectorLocation, getParentInjectorView} from './di'; -import {_getViewData, addToViewTree, createEmbeddedViewAndNode, createLContainer, createLNodeObject, createTNode, getPreviousOrParentTNode, getRenderer, renderEmbeddedTemplate} from './instructions'; -import {LContainer, RENDER_PARENT, VIEWS} from './interfaces/container'; +import {_getViewData, addToViewTree, createEmbeddedViewAndNode, createLContainer, getPreviousOrParentTNode, getRenderer, renderEmbeddedTemplate} from './instructions'; +import {ACTIVE_INDEX, LContainer, NATIVE, RENDER_PARENT, VIEWS} from './interfaces/container'; import {RenderFlags} from './interfaces/definition'; import {InjectorLocationFlags} from './interfaces/injector'; -import {LContainerNode, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeType, TViewNode} from './interfaces/node'; +import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeType, TViewNode} from './interfaces/node'; import {LQueries} from './interfaces/query'; import {RComment, RElement, Renderer3, isProceduralRenderer} from './interfaces/renderer'; import {CONTEXT, HOST_NODE, LViewData, QUERIES, RENDERER, TVIEW, TView} from './interfaces/view'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {addRemoveViewFromContainer, appendChild, detachView, findComponentView, getBeforeNodeForView, getRenderParent, insertView, removeView} from './node_manipulation'; -import {getLNode, isComponent} from './util'; +import {getLNode, isComponent, isLContainer} from './util'; import {ViewRef} from './view_ref'; @@ -122,13 +122,12 @@ export function createTemplateRef( }; } - - const hostNode = getLNode(hostTNode, hostView); + const hostContainer: LContainer = hostView[hostTNode.index]; ngDevMode && assertNodeType(hostTNode, TNodeType.Container); ngDevMode && assertDefined(hostTNode.tViews, 'TView must be allocated'); return new R3TemplateRef( hostView, createElementRef(ElementRefToken, hostTNode, hostView), hostTNode.tViews as TView, - getRenderer(), hostNode.data ![QUERIES], hostTNode.injectorIndex); + getRenderer(), hostContainer[QUERIES], hostTNode.injectorIndex); } let R3ViewContainerRef: { @@ -241,8 +240,8 @@ export function createContainerRef( insertView(lView, this._lContainer, this._hostView, adjustedIdx, this._hostTNode.index); - const container = this._getHostNode().dynamicLContainerNode !; - const beforeNode = getBeforeNodeForView(adjustedIdx, this._lContainer[VIEWS], container); + const beforeNode = + getBeforeNodeForView(adjustedIdx, this._lContainer[VIEWS], this._lContainer[NATIVE]); addRemoveViewFromContainer(lView, true, beforeNode); (viewRef as ViewRef).attachToViewContainerRef(this); @@ -283,25 +282,27 @@ export function createContainerRef( } return index; } - - private _getHostNode() { return getLNode(this._hostTNode, this._hostView); } }; } - const hostLNode = getLNode(hostTNode, hostView); ngDevMode && assertNodeOfPossibleTypes( hostTNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer); - const lContainer = createLContainer(hostView, true); - const comment = hostView[RENDERER].createComment(ngDevMode ? 'container' : ''); - const lContainerNode: LContainerNode = - createLNodeObject(TNodeType.Container, comment, lContainer); - - lContainer[RENDER_PARENT] = getRenderParent(hostTNode, hostView); + let lContainer: LContainer; + const slotValue = hostView[hostTNode.index]; + if (isLContainer(slotValue)) { + // If the host is a container, we don't need to create a new LContainer + lContainer = slotValue; + lContainer[ACTIVE_INDEX] = -1; + } else { + const comment = hostView[RENDERER].createComment(ngDevMode ? 'container' : ''); + ngDevMode && ngDevMode.rendererCreateComment++; + hostView[hostTNode.index] = lContainer = + createLContainer(slotValue, hostTNode, hostView, comment, true); - appendChild(comment, hostTNode, hostView); - hostLNode.dynamicLContainerNode = lContainerNode; - addToViewTree(hostView, hostTNode.index as number, lContainer); + appendChild(comment, hostTNode, hostView); + addToViewTree(hostView, hostTNode.index as number, lContainer); + } return new R3ViewContainerRef(lContainer, hostTNode, hostView); } diff --git a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json index 066067cf3fbf8..706ea3682a735 100644 --- a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json @@ -68,6 +68,9 @@ { "name": "HEADER_OFFSET" }, + { + "name": "HOST_NATIVE" + }, { "name": "HOST_NODE" }, @@ -86,6 +89,9 @@ { "name": "MONKEY_PATCH_KEY_NAME" }, + { + "name": "NATIVE" + }, { "name": "NEXT" }, @@ -545,9 +551,6 @@ { "name": "getComponentDef" }, - { - "name": "getContainerNode" - }, { "name": "getContainerRenderParent" }, @@ -584,6 +587,9 @@ { "name": "getInjectorIndex" }, + { + "name": "getLContainer" + }, { "name": "getLElementFromComponent" }, @@ -755,6 +761,9 @@ { "name": "isJsObject" }, + { + "name": "isLContainer" + }, { "name": "isListLikeIterable" }, @@ -782,9 +791,6 @@ { "name": "listener" }, - { - "name": "load" - }, { "name": "loadElement" }, diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index 8a62ae6139193..a128452077792 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -38,6 +38,9 @@ { "name": "HEADER_OFFSET" }, + { + "name": "HOST_NATIVE" + }, { "name": "HOST_NODE" }, @@ -50,6 +53,9 @@ { "name": "MONKEY_PATCH_KEY_NAME" }, + { + "name": "NATIVE" + }, { "name": "NEXT" }, @@ -224,9 +230,6 @@ { "name": "getComponentDef" }, - { - "name": "getContainerNode" - }, { "name": "getContainerRenderParent" }, @@ -242,6 +245,9 @@ { "name": "getInjectorIndex" }, + { + "name": "getLContainer" + }, { "name": "getLNode" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 5310263336ab5..4685600beede3 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -53,6 +53,9 @@ { "name": "HEADER_OFFSET" }, + { + "name": "HOST_NATIVE" + }, { "name": "HOST_NODE" }, @@ -71,6 +74,9 @@ { "name": "MONKEY_PATCH_KEY_NAME" }, + { + "name": "NATIVE" + }, { "name": "NEXT" }, @@ -596,9 +602,6 @@ { "name": "getComponentDef" }, - { - "name": "getContainerNode" - }, { "name": "getContainerRenderParent" }, @@ -629,6 +632,9 @@ { "name": "getInjectorIndex" }, + { + "name": "getLContainer" + }, { "name": "getLElementFromComponent" }, @@ -776,6 +782,9 @@ { "name": "isJsObject" }, + { + "name": "isLContainer" + }, { "name": "isListLikeIterable" }, @@ -800,9 +809,6 @@ { "name": "listener" }, - { - "name": "load" - }, { "name": "loadElement" }, diff --git a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json index 375f8c528c61c..883ddd126f9dc 100644 --- a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json @@ -323,6 +323,9 @@ { "name": "HOST_ATTR$1" }, + { + "name": "HOST_NATIVE" + }, { "name": "HOST_NODE" }, @@ -443,6 +446,9 @@ { "name": "NAMESPACE_URIS" }, + { + "name": "NATIVE" + }, { "name": "NATIVE_ADD_LISTENER" }, @@ -1604,9 +1610,6 @@ { "name": "getComponentDef" }, - { - "name": "getContainerNode" - }, { "name": "getContainerRenderParent" }, @@ -1667,6 +1670,9 @@ { "name": "getInjectorIndex" }, + { + "name": "getLContainer" + }, { "name": "getLElementFromComponent" }, @@ -1976,6 +1982,9 @@ { "name": "isJsObject" }, + { + "name": "isLContainer" + }, { "name": "isListLikeIterable" }, @@ -2039,9 +2048,6 @@ { "name": "listener" }, - { - "name": "load" - }, { "name": "loadElement" }, diff --git a/packages/core/test/render3/di_spec.ts b/packages/core/test/render3/di_spec.ts index 906ee80c77a3b..2e8e71d11eebc 100644 --- a/packages/core/test/render3/di_spec.ts +++ b/packages/core/test/render3/di_spec.ts @@ -13,7 +13,7 @@ import {defineComponent} from '../../src/render3/definition'; import {bloomAdd, bloomHashBitOrFactory as bloomHash, getOrCreateNodeInjector, injectAttribute, injectorHasToken} from '../../src/render3/di'; import {PublicFeature, defineDirective, directiveInject, elementProperty, load, templateRefExtractor} from '../../src/render3/index'; -import {bind, container, containerRefreshEnd, containerRefreshStart, createNodeAtIndex, createLViewData, createTView, element, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, enterView, interpolation2, leaveView, projection, projectionDef, reference, template, text, textBinding, elementContainerStart, elementContainerEnd} from '../../src/render3/instructions'; +import {bind, container, containerRefreshEnd, containerRefreshStart, createNodeAtIndex, createLViewData, createTView, element, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, enterView, interpolation2, leaveView, projection, projectionDef, reference, template, text, textBinding, elementContainerStart, elementContainerEnd, loadElement} from '../../src/render3/instructions'; import {isProceduralRenderer} from '../../src/render3/interfaces/renderer'; import {AttributeMarker, LContainerNode, LElementNode, TNodeType} from '../../src/render3/interfaces/node'; @@ -24,6 +24,7 @@ import {getRendererFactory2} from './imported_renderer2'; import {ComponentFixture, createComponent, createDirective, getDirectiveOnNode, renderComponent, toHtml} from './render_util'; import {NgIf} from './common_with_def'; import {TNODE} from '../../src/render3/interfaces/injector'; +import {LContainer, NATIVE} from '../../src/render3/interfaces/container'; describe('di', () => { describe('no dependencies', () => { @@ -1136,7 +1137,7 @@ describe('di', () => { it('should create ElementRef with comment if requesting directive is on node', () => { let dir !: Directive; - let commentNode !: LContainerNode; + let lContainer !: LContainer; class Directive { value: string; @@ -1156,13 +1157,13 @@ describe('di', () => { const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { template(0, () => {}, 0, 0, null, ['dir', '']); - commentNode = load(0); + lContainer = load(0) as any; } }, 1, 0, [Directive]); const fixture = new ComponentFixture(App); expect(dir.value).toContain('ElementRef'); - expect(dir.elementRef.nativeElement).toEqual(commentNode.native); + expect(dir.elementRef.nativeElement).toEqual(lContainer[NATIVE]); }); }); diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index 2a4e73e088a3a..fff57738f69bc 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -9,7 +9,7 @@ import {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core'; import {RendererStyleFlags2, RendererType2} from '../../src/render/api'; -import {AttributeMarker, defineComponent, defineDirective} from '../../src/render3/index'; +import {AttributeMarker, defineComponent, defineDirective, templateRefExtractor} from '../../src/render3/index'; import {NO_CHANGE, bind, container, containerRefreshEnd, containerRefreshStart, element, elementAttribute, elementClassProp, elementContainerEnd, elementContainerStart, elementEnd, elementProperty, elementStart, elementStyleProp, elementStyling, elementStylingApply, embeddedViewEnd, embeddedViewStart, enableBindings, disableBindings, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, load, projection, projectionDef, reference, text, textBinding, template} from '../../src/render3/instructions'; import {InitialStylingFlags, RenderFlags} from '../../src/render3/interfaces/definition'; @@ -1349,6 +1349,7 @@ describe('render3 integration test', () => { describe('elementClass', () => { it('should support CSS class toggle', () => { + /** */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { elementStart(0, 'span'); @@ -1412,6 +1413,72 @@ describe('render3 integration test', () => { fixture.update(); expect(fixture.html).toEqual(''); }); + + it('should apply classes properly when nodes have LContainers', () => { + let structuralComp !: StructuralComp; + + class StructuralComp { + tmp !: TemplateRef; + + constructor(public vcr: ViewContainerRef) {} + + create() { this.vcr.createEmbeddedView(this.tmp); } + + static ngComponentDef = defineComponent({ + type: StructuralComp, + selectors: [['structural-comp']], + factory: () => structuralComp = + new StructuralComp(directiveInject(ViewContainerRef as any)), + inputs: {tmp: 'tmp'}, + consts: 1, + vars: 0, + template: (rf: RenderFlags, ctx: StructuralComp) => { + if (rf & RenderFlags.Create) { + text(0, 'Comp Content'); + } + } + }); + } + + function FooTemplate(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + text(0, 'Temp Content'); + } + } + + /** + * + * Content + * + * + */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + template(0, FooTemplate, 1, 0, '', null, ['foo', ''], templateRefExtractor); + elementStart(2, 'structural-comp'); + elementStyling(['active']); + elementEnd(); + } + if (rf & RenderFlags.Update) { + const foo = reference(1) as any; + elementClassProp(2, 0, ctx.class); + elementStylingApply(2); + elementProperty(2, 'tmp', bind(foo)); + } + }, 3, 1, [StructuralComp]); + + const fixture = new ComponentFixture(App); + fixture.component.class = true; + fixture.update(); + expect(fixture.html) + .toEqual('Comp Content'); + + structuralComp.create(); + fixture.update(); + expect(fixture.html) + .toEqual('Comp ContentTemp Content'); + }); + }); });