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 2ab5ff56de)
This commit is contained in:
root
2026-08-20 01:05:06 +02:00
committed by leonsenft
parent d2e3bac33b
commit 9a8e4826b9
3 changed files with 103 additions and 3 deletions
@@ -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<C>;
}
/** 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);
}
+15 -3
View File
@@ -174,13 +174,21 @@ function createRootLViewEnvironment(rootLViewInjector: Injector): LViewEnvironme
};
}
function createHostElement(componentDef: ComponentDef<unknown>, renderer: Renderer): RElement {
function createHostElement(
componentDef: ComponentDef<unknown>,
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<T> {
environmentInjector?: NgModuleRef<any> | EnvironmentInjector | undefined,
directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[],
componentBindings?: Binding[],
hostElementNamespace?: string | null,
): AbstractComponentRef<T> {
profiler(ProfilerEvent.DynamicComponentStart);
@@ -286,6 +295,7 @@ export class ComponentFactory<T> {
rootSelectorOrNode,
directives,
componentBindings,
hostElementNamespace,
),
);
} else {
@@ -296,6 +306,7 @@ export class ComponentFactory<T> {
rootSelectorOrNode,
directives,
componentBindings,
hostElementNamespace,
);
}
} finally {
@@ -310,6 +321,7 @@ export class ComponentFactory<T> {
rootSelectorOrNode?: any,
directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[],
componentBindings?: Binding[],
hostElementNamespace?: string | null,
) {
const cmpDef = this.componentDef;
const rootTView = createRootTView(rootSelectorOrNode, cmpDef, componentBindings, directives);
@@ -317,7 +329,7 @@ export class ComponentFactory<T> {
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);
@@ -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:text>SVG content</svg:text>',
changeDetection: ChangeDetectionStrategy.Eager,
})
class SvgGroupComp {}
@Component({
template: '<svg><ng-container #container></ng-container></svg>',
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: `
<svg>
<foreignObject>
<div #container></div>
</foreignObject>
</svg>
`,
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', () => {