Skip to content

Commit f9b1038

Browse files
crisbetojasonaden
authored andcommitted
fix(ivy): content projection with Shadow DOM not working (angular#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 angular#28261
1 parent 32c61f4 commit f9b1038

File tree

2 files changed

+35
-24
lines changed

2 files changed

+35
-24
lines changed

packages/core/src/render3/node_manipulation.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {ViewEncapsulation} from '../core';
10+
911
import {attachPatchData} from './context_discovery';
1012
import {callHooks} from './hooks';
1113
import {LContainer, NATIVE, VIEWS, unusedValueExportToPlacateAjd as unused1} from './interfaces/container';
14+
import {ComponentDef} from './interfaces/definition';
1215
import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeType, TProjectionNode, TViewNode, unusedValueExportToPlacateAjd as unused2} from './interfaces/node';
1316
import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection';
1417
import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer';
@@ -486,7 +489,7 @@ function executeOnDestroys(view: LView): void {
486489
* `<component><content>delayed due to projection</content></component>`
487490
* - Parent container is disconnected: This can happen when we are inserting a view into
488491
* parent container, which itself is disconnected. For example the parent container is part
489-
* of a View which has not be inserted or is mare for projection but has not been inserted
492+
* of a View which has not be inserted or is made for projection but has not been inserted
490493
* into destination.
491494
*/
492495
function getRenderParent(tNode: TNode, currentView: LView): RElement|null {
@@ -519,15 +522,24 @@ function getRenderParent(tNode: TNode, currentView: LView): RElement|null {
519522
}
520523
} else {
521524
ngDevMode && assertNodeType(parent, TNodeType.Element);
522-
// We've got a parent which is an element in the current view. We just need to verify if the
523-
// parent element is not a component. Component's content nodes are not inserted immediately
524-
// because they will be projected, and so doing insert at this point would be wasteful.
525-
// Since the projection would then move it to its final destination.
526525
if (parent.flags & TNodeFlags.isComponent) {
527-
return null;
528-
} else {
529-
return getNativeByTNode(parent, currentView) as RElement;
526+
const tData = currentView[TVIEW].data;
527+
const tNode = tData[parent.index] as TNode;
528+
const encapsulation = (tData[tNode.directiveStart] as ComponentDef<any>).encapsulation;
529+
530+
// We've got a parent which is an element in the current view. We just need to verify if the
531+
// parent element is not a component. Component's content nodes are not inserted immediately
532+
// because they will be projected, and so doing insert at this point would be wasteful.
533+
// Since the projection would then move it to its final destination. Note that we can't
534+
// make this assumption when using the Shadow DOM, because the native projection placeholders
535+
// (<content> or <slot>) have to be in place as elements are being inserted.
536+
if (encapsulation !== ViewEncapsulation.ShadowDom &&
537+
encapsulation !== ViewEncapsulation.Native) {
538+
return null;
539+
}
530540
}
541+
542+
return getNativeByTNode(parent, currentView) as RElement;
531543
}
532544
}
533545

packages/core/test/linker/projection_integration_spec.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -381,22 +381,21 @@ describe('projection', () => {
381381
});
382382

383383
if (getDOM().supportsNativeShadowDOM()) {
384-
fixmeIvy('FW-841: Content projection with ShadovDom v0 doesn\'t work')
385-
.it('should support native content projection and isolate styles per component', () => {
386-
TestBed.configureTestingModule({declarations: [SimpleNative1, SimpleNative2]});
387-
TestBed.overrideComponent(MainComp, {
388-
set: {
389-
template: '<simple-native1><div>A</div></simple-native1>' +
390-
'<simple-native2><div>B</div></simple-native2>'
391-
}
392-
});
393-
const main = TestBed.createComponent(MainComp);
394-
395-
const childNodes = getDOM().childNodes(main.nativeElement);
396-
expect(childNodes[0]).toHaveText('div {color: red}SIMPLE1(A)');
397-
expect(childNodes[1]).toHaveText('div {color: blue}SIMPLE2(B)');
398-
main.destroy();
399-
});
384+
it('should support native content projection and isolate styles per component', () => {
385+
TestBed.configureTestingModule({declarations: [SimpleNative1, SimpleNative2]});
386+
TestBed.overrideComponent(MainComp, {
387+
set: {
388+
template: '<simple-native1><div>A</div></simple-native1>' +
389+
'<simple-native2><div>B</div></simple-native2>'
390+
}
391+
});
392+
const main = TestBed.createComponent(MainComp);
393+
394+
const childNodes = getDOM().childNodes(main.nativeElement);
395+
expect(childNodes[0]).toHaveText('div {color: red}SIMPLE1(A)');
396+
expect(childNodes[1]).toHaveText('div {color: blue}SIMPLE2(B)');
397+
main.destroy();
398+
});
400399
}
401400

402401
if (getDOM().supportsDOMEvents()) {

0 commit comments

Comments
 (0)