diff --git a/packages/core/src/render3/view_ref.ts b/packages/core/src/render3/view_ref.ts index 1dbd3211064a8..e91520690e561 100644 --- a/packages/core/src/render3/view_ref.ts +++ b/packages/core/src/render3/view_ref.ts @@ -10,10 +10,13 @@ import {ApplicationRef} from '../application_ref'; import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detection/change_detector_ref'; import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref'; import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref'; +import {assertDefined} from '../util/assert'; import {checkNoChangesInRootView, checkNoChangesInternal, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn} from './instructions/shared'; +import {CONTAINER_HEADER_OFFSET} from './interfaces/container'; import {TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node'; -import {CONTEXT, FLAGS, HOST, LView, LViewFlags, T_HOST} from './interfaces/view'; +import {CONTEXT, FLAGS, HOST, LView, LViewFlags, TView, T_HOST} from './interfaces/view'; +import {assertNodeOfPossibleTypes} from './node_assert'; import {destroyLView, renderDetachView} from './node_manipulation'; import {findComponentView, getLViewParent} from './util/view_traversal_utils'; import {getNativeByTNode, getNativeByTNodeOrNull} from './util/view_utils'; @@ -38,7 +41,7 @@ export class ViewRef implements viewEngine_EmbeddedViewRef, viewEngine_Int get rootNodes(): any[] { if (this._lView[HOST] == null) { const tView = this._lView[T_HOST] as TViewNode; - return collectNativeNodes(this._lView, tView, []); + return collectNativeNodes(this._lView, tView.child, []); } return []; } @@ -299,27 +302,38 @@ export class RootViewRef extends ViewRef { get context(): T { return null !; } } -function collectNativeNodes(lView: LView, parentTNode: TNode, result: any[]): any[] { - let tNodeChild = parentTNode.child; - - while (tNodeChild) { - const nativeNode = getNativeByTNodeOrNull(tNodeChild, lView); +function collectNativeNodes(lView: LView, tNode: TNode | null, result: any[]): any[] { + while (tNode !== null) { + ngDevMode && assertNodeOfPossibleTypes( + tNode, TNodeType.Element, TNodeType.Container, TNodeType.Projection, + TNodeType.ElementContainer); + const nativeNode = getNativeByTNodeOrNull(tNode, lView); nativeNode && result.push(nativeNode); - if (tNodeChild.type === TNodeType.ElementContainer) { - collectNativeNodes(lView, tNodeChild, result); - } else if (tNodeChild.type === TNodeType.Projection) { + if (tNode.type === TNodeType.ElementContainer) { + collectNativeNodes(lView, tNode.child, result); + } else if (tNode.type === TNodeType.Container) { + const lContainer = lView[tNode.index]; + const containerTView = tNode.tViews !as TView; + ngDevMode && assertDefined(containerTView, 'TView expected'); + const containerFirstChildTNode = containerTView.firstChild !; + if (containerFirstChildTNode !== null) { + for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) { + collectNativeNodes(lContainer[i], containerFirstChildTNode, result); + } + } + } else if (tNode.type === TNodeType.Projection) { const componentView = findComponentView(lView); const componentHost = componentView[T_HOST] as TElementNode; const parentView = getLViewParent(componentView); let currentProjectedNode: TNode|null = - (componentHost.projection as(TNode | null)[])[tNodeChild.projection as number]; + (componentHost.projection as(TNode | null)[])[tNode.projection as number]; while (currentProjectedNode && parentView) { result.push(getNativeByTNode(currentProjectedNode, parentView)); currentProjectedNode = currentProjectedNode.next; } } - tNodeChild = tNodeChild.next; + tNode = tNode.next; } return result; diff --git a/packages/core/test/acceptance/template_ref_spec.ts b/packages/core/test/acceptance/template_ref_spec.ts index d13bbcdb7e308..2a8a36980b4e2 100644 --- a/packages/core/test/acceptance/template_ref_spec.ts +++ b/packages/core/test/acceptance/template_ref_spec.ts @@ -13,47 +13,6 @@ import {onlyInIvy} from '@angular/private/testing'; describe('TemplateRef', () => { describe('rootNodes', () => { - it('should include projected nodes in rootNodes', () => { - @Component({ - selector: 'menu-content', - template: ` - - Header - - - `, - exportAs: 'menuContent' - }) - class MenuContent { - @ViewChild(TemplateRef, {static: true}) template !: TemplateRef; - } - - @Component({ - template: ` - - - - - ` - }) - class App { - @ViewChild(MenuContent) content !: MenuContent; - - constructor(public viewContainerRef: ViewContainerRef) {} - } - - TestBed.configureTestingModule({declarations: [MenuContent, App]}); - const fixture = TestBed.createComponent(App); - fixture.detectChanges(); - - const instance = fixture.componentInstance; - const viewRef = instance.viewContainerRef.createEmbeddedView(instance.content.template); - const rootNodeTextContent = viewRef.rootNodes.map(node => node && node.textContent.trim()) - .filter(text => text !== ''); - - expect(rootNodeTextContent).toEqual(['Header', 'Item one', 'Item two']); - }); - it('should return root render nodes for an embedded view instance', () => { @Component({ template: ` @@ -108,7 +67,48 @@ describe('TemplateRef', () => { expect(embeddedView.rootNodes.length).toBe(0); }); - it('should not descend into containers when retrieving root nodes', () => { + it('should include projected nodes', () => { + @Component({ + selector: 'menu-content', + template: ` + + Header + + + `, + exportAs: 'menuContent' + }) + class MenuContent { + @ViewChild(TemplateRef, {static: true}) template !: TemplateRef; + } + + @Component({ + template: ` + + + + + ` + }) + class App { + @ViewChild(MenuContent) content !: MenuContent; + + constructor(public viewContainerRef: ViewContainerRef) {} + } + + TestBed.configureTestingModule({declarations: [MenuContent, App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const instance = fixture.componentInstance; + const viewRef = instance.viewContainerRef.createEmbeddedView(instance.content.template); + const rootNodeTextContent = viewRef.rootNodes.map(node => node && node.textContent.trim()) + .filter(text => text !== ''); + + expect(rootNodeTextContent).toEqual(['Header', 'Item one', 'Item two']); + }); + + it('should descend into view containers on ng-template', () => { /** * NOTE: In VE, if `SUFFIX` text node below is _not_ present, VE will add an * additional `` comment, thus being slightly different than Ivy. @@ -116,7 +116,7 @@ describe('TemplateRef', () => { */ @Component({ template: ` - textSUFFIX + text|SUFFIX ` }) class App { @@ -131,15 +131,14 @@ describe('TemplateRef', () => { fixture.detectChanges(); const embeddedView = fixture.componentInstance.templateRef.createEmbeddedView({}); - expect(embeddedView.rootNodes.length).toBe(2); + embeddedView.detectChanges(); + + expect(embeddedView.rootNodes.length).toBe(3); expect(embeddedView.rootNodes[0].nodeType).toBe(Node.COMMENT_NODE); expect(embeddedView.rootNodes[1].nodeType).toBe(Node.TEXT_NODE); + expect(embeddedView.rootNodes[2].nodeType).toBe(Node.TEXT_NODE); }); - /** - * Contrary to containers () we _do_ descend into element containers - * () - */ it('should descend into element containers when retrieving root nodes', () => { @Component({ template: `