fix(ivy): content projection with Shadow DOM not working (#28261)

Fixes components with native content projection (using `<content>` or `<slot>`) 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
This commit is contained in:
Kristiyan Kostadinov 2019-01-23 20:00:05 +01:00 committed by Jason Aden
parent 32c61f434c
commit f9b103825a
2 changed files with 34 additions and 23 deletions

View File

@ -6,9 +6,12 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ViewEncapsulation} from '../core';
import {attachPatchData} from './context_discovery'; import {attachPatchData} from './context_discovery';
import {callHooks} from './hooks'; import {callHooks} from './hooks';
import {LContainer, NATIVE, VIEWS, unusedValueExportToPlacateAjd as unused1} from './interfaces/container'; 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 {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeType, TProjectionNode, TViewNode, unusedValueExportToPlacateAjd as unused2} from './interfaces/node';
import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection'; import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection';
import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer'; import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer';
@ -486,7 +489,7 @@ function executeOnDestroys(view: LView): void {
* `<component><content>delayed due to projection</content></component>` * `<component><content>delayed due to projection</content></component>`
* - Parent container is disconnected: This can happen when we are inserting a view into * - 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 * 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. * into destination.
*/ */
function getRenderParent(tNode: TNode, currentView: LView): RElement|null { function getRenderParent(tNode: TNode, currentView: LView): RElement|null {
@ -519,15 +522,24 @@ function getRenderParent(tNode: TNode, currentView: LView): RElement|null {
} }
} else { } else {
ngDevMode && assertNodeType(parent, TNodeType.Element); 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) { if (parent.flags & TNodeFlags.isComponent) {
return null; const tData = currentView[TVIEW].data;
} else { const tNode = tData[parent.index] as TNode;
return getNativeByTNode(parent, currentView) as RElement; const encapsulation = (tData[tNode.directiveStart] as ComponentDef<any>).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
// (<content> or <slot>) 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;
} }
} }

View File

@ -381,22 +381,21 @@ describe('projection', () => {
}); });
if (getDOM().supportsNativeShadowDOM()) { 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', () => {
.it('should support native content projection and isolate styles per component', () => { TestBed.configureTestingModule({declarations: [SimpleNative1, SimpleNative2]});
TestBed.configureTestingModule({declarations: [SimpleNative1, SimpleNative2]}); TestBed.overrideComponent(MainComp, {
TestBed.overrideComponent(MainComp, { set: {
set: { template: '<simple-native1><div>A</div></simple-native1>' +
template: '<simple-native1><div>A</div></simple-native1>' + '<simple-native2><div>B</div></simple-native2>'
'<simple-native2><div>B</div></simple-native2>' }
} });
}); const main = TestBed.createComponent(MainComp);
const main = TestBed.createComponent(MainComp);
const childNodes = getDOM().childNodes(main.nativeElement); const childNodes = getDOM().childNodes(main.nativeElement);
expect(childNodes[0]).toHaveText('div {color: red}SIMPLE1(A)'); expect(childNodes[0]).toHaveText('div {color: red}SIMPLE1(A)');
expect(childNodes[1]).toHaveText('div {color: blue}SIMPLE2(B)'); expect(childNodes[1]).toHaveText('div {color: blue}SIMPLE2(B)');
main.destroy(); main.destroy();
}); });
} }
if (getDOM().supportsDOMEvents()) { if (getDOM().supportsDOMEvents()) {