From 9a8e4826b9db4359c3f0e7185e9582b7bd08a3ef Mon Sep 17 00:00:00 2001 From: root Date: Thu, 20 Aug 2026 01:05:06 +0200 Subject: [PATCH] fix(core): preserve namespace for dynamic component hosts Forward the insertion namespace when creating dynamic component hosts inside SVG or MathML. (cherry picked from commit 2ab5ff56ded8c6246c0035d9df62a0c7fc9e1308) --- .../core/src/linker/view_container_ref.ts | 23 +++++++ packages/core/src/render3/component_ref.ts | 18 ++++- .../acceptance/view_container_ref_spec.ts | 65 +++++++++++++++++++ 3 files changed, 103 insertions(+), 3 deletions(-) diff --git a/packages/core/src/linker/view_container_ref.ts b/packages/core/src/linker/view_container_ref.ts index 0a3ce950013..2c59097bb88 100644 --- a/packages/core/src/linker/view_container_ref.ts +++ b/packages/core/src/linker/view_container_ref.ts @@ -508,6 +508,7 @@ class R3ViewContainerRef extends ViewContainerRef { environmentInjector, directives, bindings, + this._getHostElementNamespace(), ); this.insertImpl( componentRef.hostView, @@ -517,6 +518,28 @@ class R3ViewContainerRef extends ViewContainerRef { return componentRef as ComponentRef; } + /** Returns the namespace used by nodes inserted at this container's render location. */ + private _getHostElementNamespace(): string | null { + if (this._hostTNode.type & TNodeType.Element) { + const parentTNode = this._hostTNode.parent ?? this._hostLView[T_HOST]; + + // SVG foreignObject elements are namespace integration points: the foreignObject itself is + // SVG, but its children are HTML. + if ( + parentTNode !== null && + parentTNode.type & TNodeType.Element && + typeof parentTNode.value === 'string' && + parentTNode.value.toLowerCase() === 'foreignobject' + ) { + return null; + } + + return parentTNode?.namespace ?? null; + } + + return this._hostTNode.namespace; + } + override insert(viewRef: ViewRef, index?: number): ViewRef { return this.insertImpl(viewRef, index, true); } diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index 7459010045b..ff4f9e7ecf1 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -174,13 +174,21 @@ function createRootLViewEnvironment(rootLViewInjector: Injector): LViewEnvironme }; } -function createHostElement(componentDef: ComponentDef, renderer: Renderer): RElement { +function createHostElement( + componentDef: ComponentDef, + renderer: Renderer, + hostElementNamespace: string | null, +): RElement { // Determine a tag name used for creating host elements when this component is created // dynamically. Default to 'div' if this component did not specify any tag name in its // selector. const tagName = inferTagNameFromDefinition(componentDef); const namespace = - tagName === 'svg' ? SVG_NAMESPACE : tagName === 'math' ? MATH_ML_NAMESPACE : null; + tagName === 'svg' + ? SVG_NAMESPACE + : tagName === 'math' + ? MATH_ML_NAMESPACE + : hostElementNamespace; return createElementNode(renderer, tagName, namespace); } @@ -262,6 +270,7 @@ export class ComponentFactory { environmentInjector?: NgModuleRef | EnvironmentInjector | undefined, directives?: (Type | DirectiveWithBindings)[], componentBindings?: Binding[], + hostElementNamespace?: string | null, ): AbstractComponentRef { profiler(ProfilerEvent.DynamicComponentStart); @@ -286,6 +295,7 @@ export class ComponentFactory { rootSelectorOrNode, directives, componentBindings, + hostElementNamespace, ), ); } else { @@ -296,6 +306,7 @@ export class ComponentFactory { rootSelectorOrNode, directives, componentBindings, + hostElementNamespace, ); } } finally { @@ -310,6 +321,7 @@ export class ComponentFactory { rootSelectorOrNode?: any, directives?: (Type | DirectiveWithBindings)[], componentBindings?: Binding[], + hostElementNamespace?: string | null, ) { const cmpDef = this.componentDef; const rootTView = createRootTView(rootSelectorOrNode, cmpDef, componentBindings, directives); @@ -317,7 +329,7 @@ export class ComponentFactory { const hostRenderer = environment.rendererFactory.createRenderer(null, cmpDef); const hostElement = rootSelectorOrNode ? locateHostElement(hostRenderer, rootSelectorOrNode, cmpDef.encapsulation, rootViewInjector) - : createHostElement(cmpDef, hostRenderer); + : createHostElement(cmpDef, hostRenderer, hostElementNamespace ?? null); assertNotScriptHostElement(hostElement); const sharedStylesHost = rootViewInjector.get(SHARED_STYLES_HOST, null); diff --git a/packages/core/test/acceptance/view_container_ref_spec.ts b/packages/core/test/acceptance/view_container_ref_spec.ts index f349555a398..4e20e3c2295 100644 --- a/packages/core/test/acceptance/view_container_ref_spec.ts +++ b/packages/core/test/acceptance/view_container_ref_spec.ts @@ -308,6 +308,71 @@ describe('ViewContainerRef', () => { // Also test with selector that has element name in uppercase runTestWithSelectors('SVG[some-attr]', 'MATH[some-attr]'); + + it('should inherit the namespace of the insertion context for dynamic host elements', () => { + @Component({ + selector: 'g[dynamic-group]', + template: 'SVG content', + + changeDetection: ChangeDetectionStrategy.Eager, + }) + class SvgGroupComp {} + + @Component({ + template: '', + + changeDetection: ChangeDetectionStrategy.Eager, + }) + class TestComp { + @ViewChild('container', {read: ViewContainerRef}) container!: ViewContainerRef; + } + + TestBed.configureTestingModule({imports: [TestComp, SvgGroupComp]}); + const fixture = TestBed.createComponent(TestComp); + fixture.detectChanges(); + + const componentRef = fixture.componentInstance.container.createComponent(SvgGroupComp); + fixture.detectChanges(); + + expect(componentRef.location.nativeElement.namespaceURI).toBe('http://www.w3.org/2000/svg'); + expect(componentRef.location.nativeElement.tagName.toLowerCase()).toBe('g'); + }); + + it('should use the HTML namespace inside an SVG foreignObject', () => { + @Component({ + selector: 'div[dynamic-html]', + template: 'HTML content', + + changeDetection: ChangeDetectionStrategy.Eager, + }) + class HtmlComp {} + + @Component({ + template: ` + + +
+
+
+ `, + + changeDetection: ChangeDetectionStrategy.Eager, + }) + class TestComp { + @ViewChild('container', {read: ViewContainerRef}) container!: ViewContainerRef; + } + + TestBed.configureTestingModule({imports: [TestComp, HtmlComp]}); + const fixture = TestBed.createComponent(TestComp); + fixture.detectChanges(); + + const componentRef = fixture.componentInstance.container.createComponent(HtmlComp); + fixture.detectChanges(); + + expect(componentRef.location.nativeElement.namespaceURI).toBe( + 'http://www.w3.org/1999/xhtml', + ); + }); }); it('should apply attributes and classes to host element based on selector', () => {