mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(core): handle hydration of multiple nodes projected in a single slot (#53270)
This commit updates the logic to handle hydration of multiple nodes projected in a single slot. Currently, in case component nodes are content-projected and their order is changed during the projection, hydration can not find the correct element. With this fix, extra annotation info would be included for such nodes and hydration logic at runtime will use it to locate the right element. Resolves #53246. PR Close #53270
This commit is contained in:
committed by
Dylan Hunn
parent
81e9489ef2
commit
4fc1581bbc
@@ -402,6 +402,10 @@ function serializeLView(lView: LView, context: HydrationContext): SerializedView
|
||||
if (!(targetNode as HTMLElement).hasAttribute(SKIP_HYDRATION_ATTR_NAME)) {
|
||||
annotateHostElementForHydration(targetNode as RElement, lView[i], context);
|
||||
}
|
||||
// Include node path info to the annotation in case `tNode.next` (which hydration
|
||||
// relies upon by default) is different from the `tNode.projectionNext`. This helps
|
||||
// hydration runtime logic to find the right node.
|
||||
annotateNextNodePath(ngh, tNode, lView);
|
||||
} else {
|
||||
// <ng-container> case
|
||||
if (tNode.type & TNodeType.ElementContainer) {
|
||||
@@ -462,19 +466,29 @@ function serializeLView(lView: LView, context: HydrationContext): SerializedView
|
||||
}
|
||||
}
|
||||
|
||||
if (tNode.projectionNext && tNode.projectionNext !== tNode.next &&
|
||||
!isInSkipHydrationBlock(tNode.projectionNext)) {
|
||||
// Check if projection next is not the same as next, in which case
|
||||
// the node would not be found at creation time at runtime and we
|
||||
// need to provide a location for that node.
|
||||
appendSerializedNodePath(ngh, tNode.projectionNext, lView);
|
||||
}
|
||||
// Include node path info to the annotation in case `tNode.next` (which hydration
|
||||
// relies upon by default) is different from the `tNode.projectionNext`. This helps
|
||||
// hydration runtime logic to find the right node.
|
||||
annotateNextNodePath(ngh, tNode, lView);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ngh;
|
||||
}
|
||||
|
||||
/**
|
||||
* If `tNode.projectionNext` is different from `tNode.next` - it means that
|
||||
* the next `tNode` after projection is different from the one in the original
|
||||
* template. In this case we need to serialize a path to that next node, so that
|
||||
* it can be found at the right location at runtime.
|
||||
*/
|
||||
function annotateNextNodePath(ngh: SerializedView, tNode: TNode, lView: LView<unknown>) {
|
||||
if (tNode.projectionNext && tNode.projectionNext !== tNode.next &&
|
||||
!isInSkipHydrationBlock(tNode.projectionNext)) {
|
||||
appendSerializedNodePath(ngh, tNode.projectionNext, lView);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a component instance that is represented
|
||||
* by a given LView uses `ViewEncapsulation.ShadowDom`.
|
||||
|
||||
@@ -4268,6 +4268,108 @@ describe('platform-server hydration integration', () => {
|
||||
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
|
||||
});
|
||||
|
||||
it('should handle multiple nodes projected in a single slot', async () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'projector-cmp',
|
||||
template: `
|
||||
<ng-content select="foo" />
|
||||
<ng-content select="bar" />
|
||||
`,
|
||||
})
|
||||
class ProjectorCmp {
|
||||
}
|
||||
|
||||
@Component({selector: 'foo', standalone: true, template: ''})
|
||||
class FooCmp {
|
||||
}
|
||||
|
||||
@Component({selector: 'bar', standalone: true, template: ''})
|
||||
class BarCmp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [ProjectorCmp, FooCmp, BarCmp],
|
||||
selector: 'app',
|
||||
template: `
|
||||
<projector-cmp>
|
||||
<foo />
|
||||
<bar />
|
||||
<foo />
|
||||
</projector-cmp>
|
||||
`,
|
||||
})
|
||||
class SimpleComponent {
|
||||
}
|
||||
|
||||
const html = await ssr(SimpleComponent);
|
||||
const ssrContents = getAppContents(html);
|
||||
|
||||
expect(ssrContents).toContain('<app ngh');
|
||||
|
||||
resetTViewsFor(SimpleComponent, ProjectorCmp);
|
||||
|
||||
const appRef = await hydrate(html, SimpleComponent);
|
||||
const compRef = getComponentRef<SimpleComponent>(appRef);
|
||||
appRef.tick();
|
||||
|
||||
const clientRootNode = compRef.location.nativeElement;
|
||||
verifyAllNodesClaimedForHydration(clientRootNode);
|
||||
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
|
||||
});
|
||||
|
||||
it('should handle multiple nodes projected in a single slot (different order)', async () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'projector-cmp',
|
||||
template: `
|
||||
<ng-content select="foo" />
|
||||
<ng-content select="bar" />
|
||||
`,
|
||||
})
|
||||
class ProjectorCmp {
|
||||
}
|
||||
|
||||
@Component({selector: 'foo', standalone: true, template: ''})
|
||||
class FooCmp {
|
||||
}
|
||||
|
||||
@Component({selector: 'bar', standalone: true, template: ''})
|
||||
class BarCmp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [ProjectorCmp, FooCmp, BarCmp],
|
||||
selector: 'app',
|
||||
template: `
|
||||
<projector-cmp>
|
||||
<bar />
|
||||
<foo />
|
||||
<bar />
|
||||
</projector-cmp>
|
||||
`,
|
||||
})
|
||||
class SimpleComponent {
|
||||
}
|
||||
|
||||
const html = await ssr(SimpleComponent);
|
||||
const ssrContents = getAppContents(html);
|
||||
|
||||
expect(ssrContents).toContain('<app ngh');
|
||||
|
||||
resetTViewsFor(SimpleComponent, ProjectorCmp);
|
||||
|
||||
const appRef = await hydrate(html, SimpleComponent);
|
||||
const compRef = getComponentRef<SimpleComponent>(appRef);
|
||||
appRef.tick();
|
||||
|
||||
const clientRootNode = compRef.location.nativeElement;
|
||||
verifyAllNodesClaimedForHydration(clientRootNode);
|
||||
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
|
||||
});
|
||||
|
||||
it('should handle empty projection slots within <ng-container>', async () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
|
||||
Reference in New Issue
Block a user