From 6f7716268afa5146f2b2d0dbbea146defa9acfef Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 17 Jan 2025 14:07:47 +0100 Subject: [PATCH] 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 --- packages/core/src/render3/hmr.ts | 9 +++- packages/core/test/acceptance/hmr_spec.ts | 53 +++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/packages/core/src/render3/hmr.ts b/packages/core/src/render3/hmr.ts index 98231818310..5ec36916de2 100644 --- a/packages/core/src/render3/hmr.ts +++ b/packages/core/src/render3/hmr.ts @@ -109,8 +109,13 @@ function recreateMatchingLViews(oldDef: ComponentDef, 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); diff --git a/packages/core/test/acceptance/hmr_spec.ts b/packages/core/test/acceptance/hmr_spec.ts index 558cc0eb4a4..a3334d81878 100644 --- a/packages/core/test/acceptance/hmr_spec.ts +++ b/packages/core/test/acceptance/hmr_spec.ts @@ -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 world', + }; + + @Component(initialMetadata) + class ChildCmp { + vcr = inject(ViewContainerRef); + } + + @Component({ + standalone: true, + imports: [ChildCmp], + template: '', + }) + class RootCmp {} + + const fixture = TestBed.createComponent(RootCmp); + fixture.detectChanges(); + markNodesAsCreatedInitially(fixture.nativeElement); + + expectHTML( + fixture.nativeElement, + ` + + Hello world + + `, + ); + + replaceMetadata(ChildCmp, { + ...initialMetadata, + template: `Hello Bob!`, + }); + fixture.detectChanges(); + + const recreatedNodes = childrenOf(...fixture.nativeElement.querySelectorAll('child-cmp')); + verifyNodesRemainUntouched(fixture.nativeElement, recreatedNodes); + verifyNodesWereRecreated(recreatedNodes); + + expectHTML( + fixture.nativeElement, + ` + + Hello Bob! + + `, + ); + }); + describe('queries', () => { it('should update ViewChildren query results', async () => { @Component({