From f9b103825a259e59cf87c49015935f0126476d57 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 23 Jan 2019 20:00:05 +0100 Subject: [PATCH] fix(ivy): content projection with Shadow DOM not working (#28261) Fixes components with native content projection (using `` or ``) not working under Ivy. The issue comes from the fact that when creating elements inside a component, we sometimes don't append the element immediately, but we leave it to projection to move it into its final destination. This ends up breaking the native projection, because the slots have to be in place from the beginning. The following changes switch to appending the element immediately when inside a component with Shadow DOM encapsulation. This PR resolves FW-841. PR Close #28261 --- .../core/src/render3/node_manipulation.ts | 28 +++++++++++++----- .../linker/projection_integration_spec.ts | 29 +++++++++---------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 79ec137a94..a8cc9e8ffd 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -6,9 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ +import {ViewEncapsulation} from '../core'; + import {attachPatchData} from './context_discovery'; import {callHooks} from './hooks'; import {LContainer, NATIVE, VIEWS, unusedValueExportToPlacateAjd as unused1} from './interfaces/container'; +import {ComponentDef} from './interfaces/definition'; import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeType, TProjectionNode, 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'; @@ -486,7 +489,7 @@ function executeOnDestroys(view: LView): void { * `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 + * of a View which has not be inserted or is made for projection but has not been inserted * into destination. */ function getRenderParent(tNode: TNode, currentView: LView): RElement|null { @@ -519,15 +522,24 @@ function getRenderParent(tNode: TNode, currentView: LView): RElement|null { } } else { ngDevMode && assertNodeType(parent, TNodeType.Element); - // We've got a parent which is an element in the current view. We just need to verify if the - // parent element is not 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 then move it to its final destination. if (parent.flags & TNodeFlags.isComponent) { - return null; - } else { - return getNativeByTNode(parent, currentView) as RElement; + const tData = currentView[TVIEW].data; + const tNode = tData[parent.index] as TNode; + const encapsulation = (tData[tNode.directiveStart] as ComponentDef).encapsulation; + + // We've got a parent which is an element in the current view. We just need to verify if the + // parent element is not 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 then move it to its final destination. Note that we can't + // make this assumption when using the Shadow DOM, because the native projection placeholders + // ( or ) have to be in place as elements are being inserted. + if (encapsulation !== ViewEncapsulation.ShadowDom && + encapsulation !== ViewEncapsulation.Native) { + return null; + } } + + return getNativeByTNode(parent, currentView) as RElement; } } diff --git a/packages/core/test/linker/projection_integration_spec.ts b/packages/core/test/linker/projection_integration_spec.ts index 1b52edfafc..65a2233fdc 100644 --- a/packages/core/test/linker/projection_integration_spec.ts +++ b/packages/core/test/linker/projection_integration_spec.ts @@ -381,22 +381,21 @@ describe('projection', () => { }); if (getDOM().supportsNativeShadowDOM()) { - fixmeIvy('FW-841: Content projection with ShadovDom v0 doesn\'t work') - .it('should support native content projection and isolate styles per component', () => { - TestBed.configureTestingModule({declarations: [SimpleNative1, SimpleNative2]}); - TestBed.overrideComponent(MainComp, { - set: { - template: '
A
' + - '
B
' - } - }); - const main = TestBed.createComponent(MainComp); + it('should support native content projection and isolate styles per component', () => { + TestBed.configureTestingModule({declarations: [SimpleNative1, SimpleNative2]}); + TestBed.overrideComponent(MainComp, { + set: { + template: '
A
' + + '
B
' + } + }); + const main = TestBed.createComponent(MainComp); - const childNodes = getDOM().childNodes(main.nativeElement); - expect(childNodes[0]).toHaveText('div {color: red}SIMPLE1(A)'); - expect(childNodes[1]).toHaveText('div {color: blue}SIMPLE2(B)'); - main.destroy(); - }); + const childNodes = getDOM().childNodes(main.nativeElement); + expect(childNodes[0]).toHaveText('div {color: red}SIMPLE1(A)'); + expect(childNodes[1]).toHaveText('div {color: blue}SIMPLE2(B)'); + main.destroy(); + }); } if (getDOM().supportsDOMEvents()) {