From 25a2fef303edcb4319974a3845250d962afdebd1 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Sun, 24 Feb 2019 16:47:47 +0100 Subject: [PATCH] fix(ivy): ViewRef.rootNodes not including projected nodes (#28951) Currently if an embedded view contains projected nodes, its `rootNodes` array will include `null` instead of the root nodes inside the projection slot. This manifested itself in one of the Material unit tests where we stamp out a template and then move its `rootNodes` into the overlay container. This PR is related to FW-1087. PR Close #28951 --- packages/core/src/render3/view_ref.ts | 20 +++++-- .../core/test/acceptance/template_ref_spec.ts | 57 +++++++++++++++++++ .../angular_material_test_blocklist.js | 4 -- 3 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 packages/core/test/acceptance/template_ref_spec.ts diff --git a/packages/core/src/render3/view_ref.ts b/packages/core/src/render3/view_ref.ts index a25468d911..95539e1b5b 100644 --- a/packages/core/src/render3/view_ref.ts +++ b/packages/core/src/render3/view_ref.ts @@ -12,10 +12,10 @@ import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_co import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref'; import {checkNoChangesInRootView, checkNoChangesInternal, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn} from './instructions'; -import {TNode, TNodeType, TViewNode} from './interfaces/node'; -import {FLAGS, HOST, LView, LViewFlags, PARENT, T_HOST} from './interfaces/view'; +import {TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node'; +import {FLAGS, HOST, LView, LViewFlags, T_HOST} from './interfaces/view'; import {destroyLView} from './node_manipulation'; -import {getLViewParent} from './util/view_traversal_utils'; +import {findComponentView, getLViewParent} from './util/view_traversal_utils'; import {getNativeByTNode} from './util/view_utils'; @@ -291,9 +291,21 @@ function collectNativeNodes(lView: LView, parentTNode: TNode, result: any[]): an let tNodeChild = parentTNode.child; while (tNodeChild) { - result.push(getNativeByTNode(tNodeChild, lView)); + const nativeNode = getNativeByTNode(tNodeChild, lView); + nativeNode && result.push(nativeNode); if (tNodeChild.type === TNodeType.ElementContainer) { collectNativeNodes(lView, tNodeChild, result); + } else if (tNodeChild.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]; + + while (currentProjectedNode && parentView) { + result.push(getNativeByTNode(currentProjectedNode, parentView)); + currentProjectedNode = currentProjectedNode.next; + } } tNodeChild = tNodeChild.next; } diff --git a/packages/core/test/acceptance/template_ref_spec.ts b/packages/core/test/acceptance/template_ref_spec.ts new file mode 100644 index 0000000000..981cc43bac --- /dev/null +++ b/packages/core/test/acceptance/template_ref_spec.ts @@ -0,0 +1,57 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Component, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {expect} from '@angular/platform-browser/testing/src/matchers'; + +describe('TemplateRef', () => { + describe('rootNodes', () => { + it('should include projected nodes in rootNodes', () => { + @Component({ + selector: 'menu-content', + template: ` + + Header + + + `, + exportAs: 'menuContent' + }) + class MenuContent { + @ViewChild(TemplateRef) 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']); + }); + }); + +}); diff --git a/tools/material-ci/angular_material_test_blocklist.js b/tools/material-ci/angular_material_test_blocklist.js index 092c0a5e3c..e924cf2ff4 100644 --- a/tools/material-ci/angular_material_test_blocklist.js +++ b/tools/material-ci/angular_material_test_blocklist.js @@ -685,10 +685,6 @@ window.testBlocklist = { "error": "Error: Expected null to be 'mat-dialog-title-12', 'Expected the aria-labelledby to match the title id.'.", "notes": "FW-1097: Static host classes and styles don't work on root component" }, - "MatMenu should open a custom menu": { - "error": "Error: Expected function not to throw an Error, but it threw TypeError.", - "notes": "Unknown" - }, "MatMenu should close the menu when using the CloseScrollStrategy": { "error": "TypeError: Cannot read property 'openMenu' of undefined", "notes": "Unknown"