fix(core): HMR not matching component that injects ViewContainerRef (#59596)

If a component injects `ViewContainerRef`, its `LView` gets wrapped in an empty `LContainer` and the container's host becomes the `LView`. The HMR logic wasn't accounting for this which meant that such components wouldn't be replaced.

Fixes #59592.

PR Close #59596
This commit is contained in:
Kristiyan Kostadinov
2025-01-17 14:07:47 +01:00
committed by Pawel Kozlowski
parent 70c4b83509
commit 6f7716268a
2 changed files with 60 additions and 2 deletions
+7 -2
View File
@@ -109,8 +109,13 @@ function recreateMatchingLViews(oldDef: ComponentDef<unknown>, rootLView: LView)
const current = rootLView[i];
if (isLContainer(current)) {
for (let i = CONTAINER_HEADER_OFFSET; i < current.length; i++) {
recreateMatchingLViews(oldDef, current[i]);
// The host can be an LView if a component is injecting `ViewContainerRef`.
if (isLView(current[HOST])) {
recreateMatchingLViews(oldDef, current[HOST]);
}
for (let j = CONTAINER_HEADER_OFFSET; j < current.length; j++) {
recreateMatchingLViews(oldDef, current[j]);
}
} else if (isLView(current)) {
recreateMatchingLViews(oldDef, current);
+53
View File
@@ -25,6 +25,7 @@ import {
Type,
ViewChild,
ViewChildren,
ViewContainerRef,
ɵNG_COMP_DEF,
ɵɵreplaceMetadata,
} from '@angular/core';
@@ -416,6 +417,58 @@ describe('hot module replacement', () => {
verifyNodesWereRecreated(recreatedNodes);
});
it('should be able to replace a component that injects ViewContainerRef', () => {
const initialMetadata: Component = {
selector: 'child-cmp',
standalone: true,
template: 'Hello <strong>world</strong>',
};
@Component(initialMetadata)
class ChildCmp {
vcr = inject(ViewContainerRef);
}
@Component({
standalone: true,
imports: [ChildCmp],
template: '<child-cmp/>',
})
class RootCmp {}
const fixture = TestBed.createComponent(RootCmp);
fixture.detectChanges();
markNodesAsCreatedInitially(fixture.nativeElement);
expectHTML(
fixture.nativeElement,
`
<child-cmp>
Hello <strong>world</strong>
</child-cmp>
`,
);
replaceMetadata(ChildCmp, {
...initialMetadata,
template: `Hello <i>Bob</i>!`,
});
fixture.detectChanges();
const recreatedNodes = childrenOf(...fixture.nativeElement.querySelectorAll('child-cmp'));
verifyNodesRemainUntouched(fixture.nativeElement, recreatedNodes);
verifyNodesWereRecreated(recreatedNodes);
expectHTML(
fixture.nativeElement,
`
<child-cmp>
Hello <i>Bob</i>!
</child-cmp>
`,
);
});
describe('queries', () => {
it('should update ViewChildren query results', async () => {
@Component({