mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user