Skip to content

Commit 2caa45a

Browse files
crisbetopkozlowski-opensource
authored andcommitted
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
1 parent 16b0885 commit 2caa45a

File tree

2 files changed

+60
-2
lines changed

2 files changed

+60
-2
lines changed

packages/core/src/render3/hmr.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,13 @@ function recreateMatchingLViews(oldDef: ComponentDef<unknown>, rootLView: LView)
109109
const current = rootLView[i];
110110

111111
if (isLContainer(current)) {
112-
for (let i = CONTAINER_HEADER_OFFSET; i < current.length; i++) {
113-
recreateMatchingLViews(oldDef, current[i]);
112+
// The host can be an LView if a component is injecting `ViewContainerRef`.
113+
if (isLView(current[HOST])) {
114+
recreateMatchingLViews(oldDef, current[HOST]);
115+
}
116+
117+
for (let j = CONTAINER_HEADER_OFFSET; j < current.length; j++) {
118+
recreateMatchingLViews(oldDef, current[j]);
114119
}
115120
} else if (isLView(current)) {
116121
recreateMatchingLViews(oldDef, current);

packages/core/test/acceptance/hmr_spec.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
Type,
2626
ViewChild,
2727
ViewChildren,
28+
ViewContainerRef,
2829
ɵNG_COMP_DEF,
2930
ɵɵreplaceMetadata,
3031
} from '@angular/core';
@@ -416,6 +417,58 @@ describe('hot module replacement', () => {
416417
verifyNodesWereRecreated(recreatedNodes);
417418
});
418419

420+
it('should be able to replace a component that injects ViewContainerRef', () => {
421+
const initialMetadata: Component = {
422+
selector: 'child-cmp',
423+
standalone: true,
424+
template: 'Hello <strong>world</strong>',
425+
};
426+
427+
@Component(initialMetadata)
428+
class ChildCmp {
429+
vcr = inject(ViewContainerRef);
430+
}
431+
432+
@Component({
433+
standalone: true,
434+
imports: [ChildCmp],
435+
template: '<child-cmp/>',
436+
})
437+
class RootCmp {}
438+
439+
const fixture = TestBed.createComponent(RootCmp);
440+
fixture.detectChanges();
441+
markNodesAsCreatedInitially(fixture.nativeElement);
442+
443+
expectHTML(
444+
fixture.nativeElement,
445+
`
446+
<child-cmp>
447+
Hello <strong>world</strong>
448+
</child-cmp>
449+
`,
450+
);
451+
452+
replaceMetadata(ChildCmp, {
453+
...initialMetadata,
454+
template: `Hello <i>Bob</i>!`,
455+
});
456+
fixture.detectChanges();
457+
458+
const recreatedNodes = childrenOf(...fixture.nativeElement.querySelectorAll('child-cmp'));
459+
verifyNodesRemainUntouched(fixture.nativeElement, recreatedNodes);
460+
verifyNodesWereRecreated(recreatedNodes);
461+
462+
expectHTML(
463+
fixture.nativeElement,
464+
`
465+
<child-cmp>
466+
Hello <i>Bob</i>!
467+
</child-cmp>
468+
`,
469+
);
470+
});
471+
419472
describe('queries', () => {
420473
it('should update ViewChildren query results', async () => {
421474
@Component({

0 commit comments

Comments
 (0)