mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(core): include inner ViewContainerRef anchor nodes into ViewRef.rootNodes output (#49867)
Currently, the `ViewRef.rootNodes` output is missing anchor (comment) nodes for inner `ViewContainerRef`s, when an achor node was created for that instance of a `ViewContainerRef` (which happens in all cases except when an <ng-container> was used as a host for a view container). This issue affects hydration logic, which relies on the number of root nodes within a view to properly determine segments in DOM that belong to a particular view. Resolves #49849. PR Close #49867
This commit is contained in:
committed by
Jessica Janiuk
parent
e341dbdb78
commit
fedc75624c
@@ -8,11 +8,11 @@
|
||||
|
||||
import {assertParentView} from './assert';
|
||||
import {icuContainerIterate} from './i18n/i18n_tree_shaking';
|
||||
import {CONTAINER_HEADER_OFFSET} from './interfaces/container';
|
||||
import {CONTAINER_HEADER_OFFSET, NATIVE} from './interfaces/container';
|
||||
import {TIcuContainerNode, TNode, TNodeType} from './interfaces/node';
|
||||
import {RNode} from './interfaces/renderer_dom';
|
||||
import {isLContainer} from './interfaces/type_checks';
|
||||
import {DECLARATION_COMPONENT_VIEW, LView, T_HOST, TVIEW, TView} from './interfaces/view';
|
||||
import {DECLARATION_COMPONENT_VIEW, HOST, LView, T_HOST, TVIEW, TView} from './interfaces/view';
|
||||
import {assertTNodeType} from './node_assert';
|
||||
import {getProjectionNodes} from './node_manipulation';
|
||||
import {getLViewParent} from './util/view_traversal_utils';
|
||||
@@ -46,6 +46,22 @@ export function collectNativeNodes(
|
||||
lViewInAContainer[TVIEW], lViewInAContainer, lViewFirstChildTNode, result);
|
||||
}
|
||||
}
|
||||
|
||||
// When an LContainer is created, the anchor (comment) node is:
|
||||
// - (1) either reused in case of an ElementContainer (<ng-container>)
|
||||
// - (2) or a new comment node is created
|
||||
// In the first case, the anchor comment node would be added to the final
|
||||
// list by the code above (`result.push(unwrapRNode(lNode))`), but the second
|
||||
// case requires extra handling: the anchor node needs to be added to the
|
||||
// final list manually. See additional information in the `createAnchorNode`
|
||||
// function in the `view_container_ref.ts`.
|
||||
//
|
||||
// In the first case, the same reference would be stored in the `NATIVE`
|
||||
// and `HOST` slots in an LContainer. Otherwise, this is the second case and
|
||||
// we should add an element to the final list.
|
||||
if (lNode[NATIVE] !== lNode[HOST]) {
|
||||
result.push(lNode[NATIVE]);
|
||||
}
|
||||
}
|
||||
|
||||
const tNodeType = tNode.type;
|
||||
|
||||
@@ -88,11 +88,6 @@ describe('TemplateRef', () => {
|
||||
});
|
||||
|
||||
it('should descend into view containers on ng-template', () => {
|
||||
/**
|
||||
* NOTE: In VE, if `SUFFIX` text node below is _not_ present, VE will add an
|
||||
* additional `<!---->` comment, thus being slightly different than Ivy.
|
||||
* (resulting in 1 root node in Ivy and 2 in VE).
|
||||
*/
|
||||
const rootNodes = getRootNodes(`
|
||||
<ng-template #templateRef>
|
||||
<ng-template [ngIf]="true">text|</ng-template>SUFFIX
|
||||
@@ -106,29 +101,31 @@ describe('TemplateRef', () => {
|
||||
|
||||
it('should descend into view containers on an element', () => {
|
||||
/**
|
||||
* NOTE: In VE, if `SUFFIX` text node below is _not_ present, VE will add an
|
||||
* additional `<!---->` comment, thus being slightly different than Ivy.
|
||||
* (resulting in 1 root node in Ivy and 2 in VE).
|
||||
* Expected DOM structure:
|
||||
* ```
|
||||
* <div ng-reflect-ng-template-outlet="[object Object]"></div>
|
||||
* text
|
||||
* <!--container-->
|
||||
* SUFFIX
|
||||
* ```
|
||||
*/
|
||||
const rootNodes = getRootNodes(`
|
||||
<ng-template #dynamicTpl>text</ng-template>
|
||||
<ng-template #templateRef>
|
||||
<div [ngTemplateOutlet]="dynamicTpl"></div>SUFFIX
|
||||
</ng-template>
|
||||
`);
|
||||
<ng-template #dynamicTpl>text</ng-template>
|
||||
<ng-template #templateRef>
|
||||
<div [ngTemplateOutlet]="dynamicTpl"></div>SUFFIX
|
||||
</ng-template>
|
||||
`);
|
||||
|
||||
expect(rootNodes.length).toBe(3);
|
||||
expect(rootNodes.length).toBe(4);
|
||||
expect(rootNodes[0].nodeType).toBe(Node.ELEMENT_NODE);
|
||||
expect(rootNodes[1].nodeType).toBe(Node.TEXT_NODE);
|
||||
expect(rootNodes[2].nodeType).toBe(Node.TEXT_NODE);
|
||||
// This comment node is an anchor for the `ViewContainerRef`
|
||||
// created within the `NgTemplateOutlet` class.
|
||||
expect(rootNodes[2].nodeType).toBe(Node.COMMENT_NODE);
|
||||
expect(rootNodes[3].nodeType).toBe(Node.TEXT_NODE);
|
||||
});
|
||||
|
||||
it('should descend into view containers on ng-container', () => {
|
||||
/**
|
||||
* NOTE: In VE, if `SUFFIX` text node below is _not_ present, VE will add an
|
||||
* additional `<!---->` comment, thus being slightly different than Ivy.
|
||||
* (resulting in 1 root node in Ivy and 2 in VE).
|
||||
*/
|
||||
const rootNodes = getRootNodes(`
|
||||
<ng-template #dynamicTpl>text</ng-template>
|
||||
<ng-template #templateRef><ng-container [ngTemplateOutlet]="dynamicTpl"></ng-container>SUFFIX</ng-template>
|
||||
|
||||
@@ -729,6 +729,52 @@ describe('platform-server integration', () => {
|
||||
verifyAllNodesClaimedForHydration(clientRootNode);
|
||||
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
|
||||
});
|
||||
|
||||
it('should support element containers with *ngIf', async () => {
|
||||
@Component({
|
||||
selector: 'cmp',
|
||||
standalone: true,
|
||||
template: 'Hi!',
|
||||
})
|
||||
class Cmp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
imports: [NgIf],
|
||||
template: `
|
||||
<ng-container *ngIf="true">
|
||||
<div #inner></div>
|
||||
</ng-container>
|
||||
<ng-template #outer />
|
||||
`,
|
||||
})
|
||||
class SimpleComponent {
|
||||
@ViewChild('inner', {read: ViewContainerRef}) inner!: ViewContainerRef;
|
||||
@ViewChild('outer', {read: ViewContainerRef}) outer!: ViewContainerRef;
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.inner.createComponent(Cmp);
|
||||
this.outer.createComponent(Cmp);
|
||||
}
|
||||
}
|
||||
|
||||
const html = await ssr(SimpleComponent);
|
||||
const ssrContents = getAppContents(html);
|
||||
|
||||
expect(ssrContents).toContain(`<app ${NGH_ATTR_NAME}`);
|
||||
|
||||
resetTViewsFor(SimpleComponent, Cmp);
|
||||
|
||||
const appRef = await hydrate(html, SimpleComponent);
|
||||
const compRef = getComponentRef<SimpleComponent>(appRef);
|
||||
appRef.tick();
|
||||
|
||||
const clientRootNode = compRef.location.nativeElement;
|
||||
verifyAllNodesClaimedForHydration(clientRootNode);
|
||||
verifyClientAndSSRContentsMatch(ssrContents, clientRootNode);
|
||||
});
|
||||
});
|
||||
|
||||
describe('view containers', () => {
|
||||
|
||||
Reference in New Issue
Block a user