diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index ecd0bf65f4..33d7eb5971 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -19,16 +19,16 @@ import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, ViewRef as viewEngine_Vie import {Type} from '../type'; import {assertDefined, assertGreaterThan, assertLessThan} from './assert'; -import {addToViewTree, assertPreviousIsParent, createLContainer, createLNodeObject, createTNode, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate, resolveDirective} from './instructions'; +import {addToViewTree, assertPreviousIsParent, createEmbeddedViewNode, createLContainer, createLNodeObject, createTNode, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate, resolveDirective} from './instructions'; import {VIEWS} from './interfaces/container'; -import {ComponentTemplate, DirectiveDefInternal} from './interfaces/definition'; +import {ComponentTemplate, DirectiveDefInternal, RenderFlags} from './interfaces/definition'; import {LInjector} from './interfaces/injector'; import {AttributeMarker, LContainerNode, LElementNode, LNode, LViewNode, TNodeFlags, TNodeType} from './interfaces/node'; import {LQueries, QueryReadType} from './interfaces/query'; import {Renderer3} from './interfaces/renderer'; import {DIRECTIVES, HOST_NODE, INJECTOR, LViewData, QUERIES, RENDERER, TVIEW, TView} from './interfaces/view'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; -import {appendChild, detachView, getParentLNode, insertView, removeView} from './node_manipulation'; +import {addRemoveViewFromContainer, appendChild, detachView, getChildLNode, getParentLNode, insertView, removeView} from './node_manipulation'; import {notImplemented, stringify} from './util'; import {EmbeddedViewRef, ViewRef} from './view_ref'; @@ -631,8 +631,11 @@ class ViewContainerRef implements viewEngine_ViewContainerRef { createEmbeddedView(templateRef: viewEngine_TemplateRef, context?: C, index?: number): viewEngine_EmbeddedViewRef { - const viewRef = templateRef.createEmbeddedView(context || {}); - this.insert(viewRef, index); + const adjustedIdx = this._adjustIndex(index); + const viewRef = (templateRef as TemplateRef) + .createEmbeddedView(context || {}, this._lContainerNode, adjustedIdx); + (viewRef as EmbeddedViewRef).attachToViewContainerRef(this); + this._viewRefs.splice(adjustedIdx, 0, viewRef); return viewRef; } @@ -650,11 +653,15 @@ class ViewContainerRef implements viewEngine_ViewContainerRef { const lViewNode = (viewRef as EmbeddedViewRef)._lViewNode; const adjustedIdx = this._adjustIndex(index); - (viewRef as EmbeddedViewRef).attachToViewContainerRef(this); - - insertView(this._lContainerNode, lViewNode, adjustedIdx); lViewNode.dynamicParent = this._lContainerNode; + insertView(this._lContainerNode, lViewNode, adjustedIdx); + const views = this._lContainerNode.data[VIEWS]; + const beforeNode = adjustedIdx + 1 < views.length ? + (getChildLNode(views[adjustedIdx + 1]) !).native : + this._lContainerNode.native; + addRemoveViewFromContainer(this._lContainerNode, lViewNode, true, beforeNode); + (viewRef as EmbeddedViewRef).attachToViewContainerRef(this); this._viewRefs.splice(adjustedIdx, 0, viewRef); return viewRef; @@ -724,9 +731,14 @@ class TemplateRef implements viewEngine_TemplateRef { this.elementRef = elementRef; } - createEmbeddedView(context: T): viewEngine_EmbeddedViewRef { - const viewNode = - renderEmbeddedTemplate(null, this._tView, context, this._renderer, this._queries); + createEmbeddedView(context: T, containerNode?: LContainerNode, index?: number): + viewEngine_EmbeddedViewRef { + const viewNode = createEmbeddedViewNode(this._tView, context, this._renderer, this._queries); + if (containerNode) { + viewNode.dynamicParent = containerNode; + insertView(containerNode, viewNode, index !); + } + renderEmbeddedTemplate(viewNode, this._tView, context, RenderFlags.Create); return new EmbeddedViewRef(viewNode, this._tView.template !as ComponentTemplate, context); } } diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts index 9871b2e56f..f8eed32174 100644 --- a/packages/core/src/render3/i18n.ts +++ b/packages/core/src/render3/i18n.ts @@ -8,6 +8,7 @@ import {assertEqual, assertLessThan} from './assert'; import {NO_CHANGE, bindingUpdated, createLNode, getPreviousOrParentNode, getRenderer, getViewData, load} from './instructions'; +import {RENDER_PARENT} from './interfaces/container'; import {LContainerNode, LElementNode, LNode, TNodeType} from './interfaces/node'; import {BINDING_INDEX} from './interfaces/view'; import {appendChild, createTextNode, getParentLNode, removeChild} from './node_manipulation'; @@ -320,6 +321,7 @@ export function i18nApply(startIndex: number, instructions: I18nInstruction[]): if (removedNode.tNode.type === TNodeType.Container && removedNode.dynamicLContainerNode) { removeChild(parentNode, removedNode.dynamicLContainerNode.native || null, viewData); removedNode.dynamicLContainerNode.tNode.detached = true; + removedNode.dynamicLContainerNode.data[RENDER_PARENT] = null; } break; } diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 3f2b19c279..f4684373b8 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -6,19 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ +import './ng_dev_mode'; + import {Sanitizer} from '../sanitization/security'; + import {assertDefined, assertEqual, assertLessThan, assertNotDefined, assertNotEqual} from './assert'; import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors'; import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks'; import {ACTIVE_INDEX, LContainer, RENDER_PARENT, VIEWS} from './interfaces/container'; import {LInjector} from './interfaces/injector'; -import {AttributeMarker, InitialInputData, InitialInputs, LContainerNode, LElementNode, LNode, LProjectionNode, LTextNode, LViewNode, PropertyAliases, PropertyAliasValue, TAttributes, TContainerNode, TElementNode, TNode, TNodeFlags, TNodeType,} from './interfaces/node'; import {CssSelectorList, LProjection, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'; import {LQueries} from './interfaces/query'; -import {BINDING_INDEX, CLEANUP, CONTEXT, CurrentMatchesList, DIRECTIVES, FLAGS, HEADER_OFFSET, HOST_NODE, INJECTOR, LViewData, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RootContext, SANITIZER, TAIL, TVIEW, TView,} from './interfaces/view'; -import './ng_dev_mode'; -import {assertNodeType} from './node_assert'; -import {appendChild, appendProjectedNode, canInsertNativeNode, createTextNode, getChildLNode, getLViewChild, getNextLNode, getParentLNode, insertView, removeChild, removeView} from './node_manipulation'; +import {BINDING_INDEX, CLEANUP, 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 {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'; @@ -463,6 +466,30 @@ export function renderTemplate( return host; } +/** + * Used for creating the LViewNode of a dynamic embedded view, + * either through ViewContainerRef.createEmbeddedView() or TemplateRef.createEmbeddedView(). + * Such lViewNode will then be renderer with renderEmbeddedTemplate() (see below). + */ +export function createEmbeddedViewNode( + tView: TView, context: T, renderer: Renderer3, queries?: LQueries | null): LViewNode { + const _isParent = isParent; + const _previousOrParentNode = previousOrParentNode; + isParent = true; + previousOrParentNode = null !; + + const lView = + createLViewData(renderer, tView, context, LViewFlags.CheckAlways, getCurrentSanitizer()); + if (queries) { + lView[QUERIES] = queries.createView(); + } + const viewNode = createLNode(-1, TNodeType.View, null, null, null, lView); + + isParent = _isParent; + previousOrParentNode = _previousOrParentNode; + return viewNode; +} + /** * Used for rendering embedded views (e.g. dynamically created views) * @@ -474,27 +501,14 @@ export function renderTemplate( * TView for dynamically created views on their host TNode, which only has one instance. */ export function renderEmbeddedTemplate( - viewNode: LViewNode | null, tView: TView, context: T, renderer: Renderer3, - queries?: LQueries | null): LViewNode { + viewNode: LViewNode, tView: TView, context: T, rf: RenderFlags): LViewNode { const _isParent = isParent; const _previousOrParentNode = previousOrParentNode; let oldView: LViewData; - let rf: RenderFlags = RenderFlags.Update; try { isParent = true; previousOrParentNode = null !; - if (viewNode == null) { - const lView = - createLViewData(renderer, tView, context, LViewFlags.CheckAlways, getCurrentSanitizer()); - - if (queries) { - lView[QUERIES] = queries.createView(); - } - - viewNode = createLNode(-1, TNodeType.View, null, null, null, lView); - rf = RenderFlags.Create; - } oldView = enterView(viewNode.data, viewNode); namespaceHTML(); tView.template !(rf, context); @@ -1575,14 +1589,19 @@ function generateInitialInputs( export function createLContainer( parentLNode: LNode, currentView: LViewData, isForViewContainerRef?: boolean): LContainer { ngDevMode && assertDefined(parentLNode, 'containers should have a parent'); + let renderParent = canInsertNativeNode(parentLNode, currentView) ? + parentLNode as LElementNode | LViewNode : + null; + if (renderParent && renderParent.tNode.type === TNodeType.View) { + renderParent = getParentLNode(renderParent as LViewNode) !.data[RENDER_PARENT]; + } return [ isForViewContainerRef ? null : 0, // active index currentView, // parent null, // next null, // queries [], // views - canInsertNativeNode(parentLNode, currentView) ? parentLNode as LElementNode : - null // renderParent + renderParent as LElementNode ]; } @@ -1697,7 +1716,7 @@ function refreshDynamicEmbeddedViews(lViewData: LViewData) { const dynamicViewData = lViewNode.data; ngDevMode && assertDefined(dynamicViewData[TVIEW], 'TView must be allocated'); renderEmbeddedTemplate( - lViewNode, dynamicViewData[TVIEW], dynamicViewData[CONTEXT] !, renderer); + lViewNode, dynamicViewData[TVIEW], dynamicViewData[CONTEXT] !, RenderFlags.Update); } } } @@ -1764,6 +1783,17 @@ export function embeddedViewStart(viewBlockId: number): RenderFlags { enterView( newView, viewNode = createLNode(viewBlockId, TNodeType.View, null, null, null, newView)); } + const containerNode = getParentLNode(viewNode) as LContainerNode; + if (containerNode) { + ngDevMode && assertNodeType(viewNode, TNodeType.View); + ngDevMode && assertNodeType(containerNode, TNodeType.Container); + const lContainer = containerNode.data; + if (creationMode) { + // it is a new view, insert it into collection of views for a given container + insertView(containerNode, viewNode, lContainer[ACTIVE_INDEX] !); + } + lContainer[ACTIVE_INDEX] !++; + } return getRenderFlags(viewNode.data); } @@ -1794,22 +1824,17 @@ function getOrCreateEmbeddedTView(viewIndex: number, parent: LContainerNode): TV export function embeddedViewEnd(): void { refreshView(); isParent = false; - const viewNode = previousOrParentNode = viewData[HOST_NODE] as LViewNode; - const containerNode = getParentLNode(previousOrParentNode) as LContainerNode; - if (containerNode) { - ngDevMode && assertNodeType(viewNode, TNodeType.View); - ngDevMode && assertNodeType(containerNode, TNodeType.Container); - const lContainer = containerNode.data; - - if (creationMode) { + previousOrParentNode = viewData[HOST_NODE] as LViewNode; + if (creationMode) { + const containerNode = getParentLNode(previousOrParentNode) as LContainerNode; + if (containerNode) { + ngDevMode && assertNodeType(previousOrParentNode, TNodeType.View); + ngDevMode && assertNodeType(containerNode, TNodeType.Container); // When projected nodes are going to be inserted, the renderParent of the dynamic container // used by the ViewContainerRef must be set. - setRenderParentInProjectedNodes(lContainer[RENDER_PARENT], viewNode); - // it is a new view, insert it into collection of views for a given container - insertView(containerNode, viewNode, lContainer[ACTIVE_INDEX] !); + setRenderParentInProjectedNodes( + containerNode.data[RENDER_PARENT], previousOrParentNode as LViewNode); } - - lContainer[ACTIVE_INDEX] !++; } leaveView(viewData[PARENT] !); ngDevMode && assertEqual(isParent, false, 'isParent'); @@ -2002,7 +2027,7 @@ export function projection( const currentParent = getParentLNode(node); if (canInsertNativeNode(currentParent, viewData)) { - ngDevMode && assertNodeType(currentParent, TNodeType.Element); + ngDevMode && assertNodeOfPossibleTypes(currentParent, TNodeType.Element, TNodeType.View); // process each node in the list of projected nodes: let nodeToProject: LNode|null = node.data.head; const lastNodeToProject = node.data.tail; diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 25c725e5a7..58a19e1965 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -12,7 +12,7 @@ import {LContainerNode, LElementNode, LNode, LProjectionNode, LTextNode, LViewNo import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection'; import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer'; import {CLEANUP, DIRECTIVES, 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 {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {stringify} from './util'; const unusedValueToPlacateAjd = unused1 + unused2 + unused3 + unused4 + unused5; @@ -313,16 +313,6 @@ export function insertView( lView[QUERIES] !.insertView(index); } - // If the container's renderParent is null, we know that it is a root node of its own parent view - // and we should wait until that parent processes its nodes (otherwise, we will insert this view's - // nodes twice - once now and once when its parent inserts its views). - if (container.data[RENDER_PARENT] !== null && !container.tNode.detached) { - // Find the node to insert in front of - const beforeNode = - index + 1 < views.length ? (getChildLNode(views[index + 1]) !).native : container.native; - addRemoveViewFromContainer(container, viewNode, true, beforeNode); - } - // Sets the attached flag viewNode.data[FLAGS] |= LViewFlags.Attached; @@ -485,27 +475,66 @@ function executePipeOnDestroys(viewData: LViewData): void { } /** - * Returns whether a native element should be inserted in the given parent. + * Returns whether a native element can be inserted into the given parent. + * + * There are two reasons why we may not be able to insert a element immediately. + * - Projection: When creating a child content element of a component, we have to skip the + * insertion because the content of a component will be projected. + * `delayed due to projection` + * - Parent container is disconnected: This can happen when we are inserting a view into + * parent container, which itself is disconnected. For example the parent container is part + * of a View which has not be inserted or is mare for projection but has not been inserted + * into destination. * - * The native node can be inserted when its parent is: - * - A regular element => Yes - * - A component host element => - * - if the `currentView` === the parent `view`: The element is in the content (vs the - * template) - * => don't add as the parent component will project if needed. - * - `currentView` !== the parent `view` => The element is in the template (vs the content), - * add it - * - View element => delay insertion, will be done on `viewEnd()` + * - * @param parent The parent in which to insert the child - * @param currentView The LView being processed - * @return boolean Whether the child element should be inserted. + * @param parent The parent where the child will be inserted into. + * @param currentView Current LView being processed. + * @return boolean Whether the child should be inserted now (or delayed until later). */ export function canInsertNativeNode(parent: LNode, currentView: LViewData): boolean { - const parentIsElement = parent.tNode.type === TNodeType.Element; + // We can only insert into a Component or View. Any other type should be an Error. + ngDevMode && assertNodeOfPossibleTypes(parent, TNodeType.Element, TNodeType.View); - return parentIsElement && - (parent.view !== currentView || parent.data === null /* Regular Element. */); + if (parent.tNode.type === TNodeType.Element) { + // Parent is an element. + if (parent.view !== currentView) { + // If the Parent view is not the same as current view than we are inserting across + // Views. This happens when we insert a root element of the component view into + // the component host element and it should always be eager. + return true; + } + // Parent elements can be a component which may have projection. + if (parent.data === null) { + // Parent is a regular non-component element. We should eagerly insert into it + // since we know that this relationship will never be broken. + return true; + } else { + // Parent is a Component. Component's content nodes are not inserted immediately + // because they will be projected, and so doing insert at this point would be wasteful. + // Since the projection would than move it to its final destination. + return false; + } + } else { + // Parent is a View. + ngDevMode && assertNodeType(parent, TNodeType.View); + + // Because we are inserting into a `View` the `View` may be disconnected. + const grandParentContainer = getParentLNode(parent) as LContainerNode; + if (grandParentContainer == null) { + // The `View` is not inserted into a `Container` we have to delay insertion. + return false; + } + ngDevMode && assertNodeType(grandParentContainer, TNodeType.Container); + if (grandParentContainer.data[RENDER_PARENT] == null) { + // The parent `Container` itself is disconnected. So we have to delay. + return false; + } else { + // The parent `Container` is in inserted state, so we can eagerly insert into + // this location. + return true; + } + } } /** @@ -520,10 +549,21 @@ export function canInsertNativeNode(parent: LNode, currentView: LViewData): bool */ export function appendChild(parent: LNode, child: RNode | null, currentView: LViewData): boolean { if (child !== null && canInsertNativeNode(parent, currentView)) { - // We only add the element if not in View or not projected. const renderer = currentView[RENDERER]; - isProceduralRenderer(renderer) ? renderer.appendChild(parent.native !as RElement, child) : - parent.native !.appendChild(child); + if (parent.tNode.type === TNodeType.View) { + const container = getParentLNode(parent) as LContainerNode; + const renderParent = container.data[RENDER_PARENT]; + const views = container.data[VIEWS]; + const index = views.indexOf(parent as LViewNode); + const beforeNode = + index + 1 < views.length ? (getChildLNode(views[index + 1]) !).native : container.native; + isProceduralRenderer(renderer) ? + renderer.insertBefore(renderParent !.native, child, beforeNode) : + renderParent !.native.insertBefore(child, beforeNode, true); + } else { + isProceduralRenderer(renderer) ? renderer.appendChild(parent.native !as RElement, child) : + parent.native !.appendChild(child); + } return true; } return false; 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 6bb70e8f53..231741d6aa 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -57,10 +57,10 @@ "name": "RENDERER" }, { - "name": "ROOT_DIRECTIVE_INDICES" + "name": "RENDER_PARENT" }, { - "name": "SANITIZER" + "name": "ROOT_DIRECTIVE_INDICES" }, { "name": "TVIEW" @@ -156,7 +156,7 @@ "name": "firstTemplatePass" }, { - "name": "getCurrentSanitizer" + "name": "getChildLNode" }, { "name": "getDirectiveInstance" diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 737f86ebae..9a7455412e 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -83,6 +83,9 @@ { "name": "RENDERER" }, + { + "name": "RENDER_PARENT" + }, { "name": "ROOT_DIRECTIVE_INDICES" }, @@ -335,6 +338,9 @@ { "name": "generatePropertyAliases" }, + { + "name": "getChildLNode" + }, { "name": "getCleanup" }, diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index cb4e31f50a..4031992eb8 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -261,6 +261,11 @@ describe('ViewContainerRef', () => { insertTpl(ctx: {}) { this._vcRef.createEmbeddedView(this._tplRef, ctx); } + insertTpl2(ctx: {}) { + const viewRef = this._tplRef.createEmbeddedView(ctx); + this._vcRef.insert(viewRef); + } + remove(index?: number) { this._vcRef.remove(index); } } @@ -327,7 +332,7 @@ describe('ViewContainerRef', () => { fixture.update(); expect(fixture.html).toEqual('before|A|after'); - directiveInstance !.insertTpl({}); + directiveInstance !.insertTpl2({}); expect(fixture.html).toEqual('before|AA|after'); fixture.component.condition = true;