Skip to content

Commit ae0253f

Browse files
AndrewKushnirkara
authored andcommitted
fix(ivy): set namespace for host elements of dynamically created components (angular#35136)
Prior to this change, element namespace was not set for host elements of dynamically created components that resulted in incorrect rendering in a browser. This commit adds the logic to pick and set correct namespace for host element when component is created dynamically. PR Close angular#35136
1 parent d5d9971 commit ae0253f

File tree

6 files changed

+127
-12
lines changed

6 files changed

+127
-12
lines changed

packages/core/src/render3/component_ref.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {RendererFactory2} from '../render/api';
1919
import {Sanitizer} from '../sanitization/sanitizer';
2020
import {VERSION} from '../version';
2121
import {NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from '../view/provider';
22+
2223
import {assertComponentType} from './assert';
2324
import {LifecycleHooksFeature, createRootComponent, createRootComponentView, createRootContext} from './component';
2425
import {getComponentDef} from './definition';
@@ -28,6 +29,7 @@ import {ComponentDef} from './interfaces/definition';
2829
import {TContainerNode, TElementContainerNode, TElementNode} from './interfaces/node';
2930
import {RNode, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
3031
import {LView, LViewFlags, TVIEW, TViewType} from './interfaces/view';
32+
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces';
3133
import {stringifyCSSSelectorList} from './node_selector_matcher';
3234
import {enterView, leaveView} from './state';
3335
import {defaultScheduler} from './util/misc_utils';
@@ -59,6 +61,11 @@ function toRefArray(map: {[key: string]: string}): {propName: string; templateNa
5961
return array;
6062
}
6163

64+
function getNamespace(elementName: string): string|null {
65+
const name = elementName.toLowerCase();
66+
return name === 'svg' ? SVG_NAMESPACE : (name === 'math' ? MATH_ML_NAMESPACE : null);
67+
}
68+
6269
/**
6370
* A change detection scheduler token for {@link RootContext}. This token is the default value used
6471
* for the default `RootContext` found in the {@link ROOT_CONTEXT} token.
@@ -132,14 +139,14 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
132139
const sanitizer = rootViewInjector.get(Sanitizer, null);
133140

134141
const hostRenderer = rendererFactory.createRenderer(null, this.componentDef);
142+
// Determine a tag name used for creating host elements when this component is created
143+
// dynamically. Default to 'div' if this component did not specify any tag name in its selector.
144+
const elementName = this.componentDef.selectors[0][0] as string || 'div';
135145
const hostRNode = rootSelectorOrNode ?
136146
locateHostElement(hostRenderer, rootSelectorOrNode, this.componentDef.encapsulation) :
137-
// Determine a tag name used for creating host elements when this component is created
138-
// dynamically. Default to 'div' if this component did not specify any tag name in its
139-
// selector.
140147
elementCreate(
141-
this.componentDef.selectors[0][0] as string || 'div',
142-
rendererFactory.createRenderer(null, this.componentDef), null);
148+
elementName, rendererFactory.createRenderer(null, this.componentDef),
149+
getNamespace(elementName));
143150

144151
const rootFlags = this.componentDef.onPush ? LViewFlags.Dirty | LViewFlags.IsRoot :
145152
LViewFlags.CheckAlways | LViewFlags.IsRoot;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
10+
export const MATH_ML_NAMESPACE = 'http://www.w3.org/1998/MathML/';

packages/core/src/render3/state.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {assertDefined} from '../util/assert';
1111
import {assertLViewOrUndefined} from './assert';
1212
import {TNode} from './interfaces/node';
1313
import {CONTEXT, DECLARATION_VIEW, LView, OpaqueViewState, TVIEW, TView} from './interfaces/view';
14+
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces';
1415

1516

1617
/**
@@ -511,7 +512,7 @@ export function setSelectedIndex(index: number) {
511512
* @codeGenApi
512513
*/
513514
export function ɵɵnamespaceSVG() {
514-
instructionState.lFrame.currentNamespace = 'http://www.w3.org/2000/svg';
515+
instructionState.lFrame.currentNamespace = SVG_NAMESPACE;
515516
}
516517

517518
/**
@@ -520,7 +521,7 @@ export function ɵɵnamespaceSVG() {
520521
* @codeGenApi
521522
*/
522523
export function ɵɵnamespaceMathML() {
523-
instructionState.lFrame.currentNamespace = 'http://www.w3.org/1998/MathML/';
524+
instructionState.lFrame.currentNamespace = MATH_ML_NAMESPACE;
524525
}
525526

526527
/**

packages/core/test/acceptance/view_container_ref_spec.ts

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88

99
import {CommonModule, DOCUMENT} from '@angular/common';
1010
import {computeMsgId} from '@angular/compiler';
11-
import {Compiler, Component, ComponentFactoryResolver, Directive, DoCheck, ElementRef, EmbeddedViewRef, ErrorHandler, NO_ERRORS_SCHEMA, NgModule, OnInit, Pipe, PipeTransform, QueryList, RendererFactory2, RendererType2, Sanitizer, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core';
11+
import {Compiler, Component, ComponentFactoryResolver, Directive, DoCheck, ElementRef, EmbeddedViewRef, ErrorHandler, NO_ERRORS_SCHEMA, NgModule, OnInit, Pipe, PipeTransform, QueryList, Renderer2, RendererFactory2, RendererType2, Sanitizer, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵsetDocument} from '@angular/core';
1212
import {Input} from '@angular/core/src/metadata';
1313
import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode';
1414
import {TestBed, TestComponentRenderer} from '@angular/core/testing';
1515
import {clearTranslations, loadTranslations} from '@angular/localize';
16-
import {By, DomSanitizer} from '@angular/platform-browser';
16+
import {By, DomSanitizer, ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser';
1717
import {expect} from '@angular/platform-browser/testing/src/matchers';
1818
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
1919

@@ -160,6 +160,97 @@ describe('ViewContainerRef', () => {
160160
fixture.detectChanges();
161161
expect(fixture.debugElement.nativeElement.innerHTML).toContain('Hello');
162162
});
163+
164+
describe('element namespaces', () => {
165+
function runTestWithSelectors(svgSelector: string, mathMLSelector: string) {
166+
it('should be set correctly for host elements of dynamically created components', () => {
167+
@Component({
168+
selector: svgSelector,
169+
template: '<svg><g></g></svg>',
170+
})
171+
class SvgComp {
172+
}
173+
174+
@Component({
175+
selector: mathMLSelector,
176+
template: '<math><matrix></matrix></math>',
177+
})
178+
class MathMLComp {
179+
}
180+
181+
@NgModule({
182+
entryComponents: [SvgComp, MathMLComp],
183+
declarations: [SvgComp, MathMLComp],
184+
// View Engine doesn't have MathML tags listed in `DomElementSchemaRegistry`, thus
185+
// throwing "unknown element" error (':math:matrix' is not a known element). Ignore
186+
// these errors by adding `NO_ERRORS_SCHEMA` to this NgModule.
187+
schemas: [NO_ERRORS_SCHEMA],
188+
})
189+
class RootModule {
190+
}
191+
192+
@Component({
193+
template: `
194+
<ng-container #svg></ng-container>
195+
<ng-container #mathml></ng-container>
196+
`
197+
})
198+
class TestComp {
199+
@ViewChild('svg', {read: ViewContainerRef}) svgVCRef !: ViewContainerRef;
200+
@ViewChild('mathml', {read: ViewContainerRef}) mathMLVCRef !: ViewContainerRef;
201+
202+
constructor(public cfr: ComponentFactoryResolver) {}
203+
204+
createDynamicComponents() {
205+
const svgFactory = this.cfr.resolveComponentFactory(SvgComp);
206+
this.svgVCRef.createComponent(svgFactory);
207+
208+
const mathMLFactory = this.cfr.resolveComponentFactory(MathMLComp);
209+
this.mathMLVCRef.createComponent(mathMLFactory);
210+
}
211+
}
212+
213+
function _document(): any {
214+
// Tell Ivy about the global document
215+
ɵsetDocument(document);
216+
return document;
217+
}
218+
219+
TestBed.configureTestingModule({
220+
declarations: [TestComp],
221+
imports: [RootModule],
222+
providers: [
223+
{provide: DOCUMENT, useFactory: _document, deps: []},
224+
// TODO(FW-811): switch back to default server renderer (i.e. remove the line below)
225+
// once it starts to support Ivy namespace format (URIs) correctly. For now, use
226+
// `DomRenderer` that supports Ivy namespace format.
227+
{provide: RendererFactory2, useClass: DomRendererFactory2}
228+
],
229+
});
230+
const fixture = TestBed.createComponent(TestComp);
231+
fixture.detectChanges();
232+
233+
fixture.componentInstance.createDynamicComponents();
234+
fixture.detectChanges();
235+
236+
expect(fixture.nativeElement.querySelector('svg').namespaceURI)
237+
.toEqual('http://www.w3.org/2000/svg');
238+
239+
// View Engine doesn't set MathML namespace, since it's not present in the list of
240+
// known namespaces here:
241+
// https://github.com/angular/angular/blob/master/packages/platform-browser/src/dom/dom_renderer.ts#L14
242+
if (ivyEnabled) {
243+
expect(fixture.nativeElement.querySelector('math').namespaceURI)
244+
.toEqual('http://www.w3.org/1998/MathML/');
245+
}
246+
});
247+
}
248+
249+
runTestWithSelectors('svg[some-attr]', 'math[some-attr]');
250+
251+
// Also test with selector that has element name in uppercase
252+
runTestWithSelectors('SVG[some-attr]', 'MATH[some-attr]');
253+
});
163254
});
164255

165256
describe('insert', () => {

packages/platform-browser/src/dom/dom_renderer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ class DefaultDomRenderer2 implements Renderer2 {
174174
setAttribute(el: any, name: string, value: string, namespace?: string): void {
175175
if (namespace) {
176176
name = namespace + ':' + name;
177-
// TODO(benlesh): Ivy may cause issues here because it's passing around
177+
// TODO(FW-811): Ivy may cause issues here because it's passing around
178178
// full URIs for namespaces, therefore this lookup will fail.
179179
const namespaceUri = NAMESPACE_URIS[namespace];
180180
if (namespaceUri) {
@@ -189,13 +189,13 @@ class DefaultDomRenderer2 implements Renderer2 {
189189

190190
removeAttribute(el: any, name: string, namespace?: string): void {
191191
if (namespace) {
192-
// TODO(benlesh): Ivy may cause issues here because it's passing around
192+
// TODO(FW-811): Ivy may cause issues here because it's passing around
193193
// full URIs for namespaces, therefore this lookup will fail.
194194
const namespaceUri = NAMESPACE_URIS[namespace];
195195
if (namespaceUri) {
196196
el.removeAttributeNS(namespaceUri, name);
197197
} else {
198-
// TODO(benlesh): Since ivy is passing around full URIs for namespaces
198+
// TODO(FW-811): Since ivy is passing around full URIs for namespaces
199199
// this could result in properties like `http://www.w3.org/2000/svg:cx="123"`,
200200
// which is wrong.
201201
el.removeAttribute(`${namespace}:${name}`);

packages/platform-server/src/server_renderer.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ class DefaultServerRenderer2 implements Renderer2 {
7373
createElement(name: string, namespace?: string, debugInfo?: any): any {
7474
if (namespace) {
7575
const doc = this.document || getDOM().getDefaultDocument();
76+
// TODO(FW-811): Ivy may cause issues here because it's passing around
77+
// full URIs for namespaces, therefore this lookup will fail.
7678
return doc.createElementNS(NAMESPACE_URIS[namespace], name);
7779
}
7880

@@ -124,6 +126,8 @@ class DefaultServerRenderer2 implements Renderer2 {
124126

125127
setAttribute(el: any, name: string, value: string, namespace?: string): void {
126128
if (namespace) {
129+
// TODO(FW-811): Ivy may cause issues here because it's passing around
130+
// full URIs for namespaces, therefore this lookup will fail.
127131
el.setAttributeNS(NAMESPACE_URIS[namespace], namespace + ':' + name, value);
128132
} else {
129133
el.setAttribute(name, value);
@@ -132,6 +136,8 @@ class DefaultServerRenderer2 implements Renderer2 {
132136

133137
removeAttribute(el: any, name: string, namespace?: string): void {
134138
if (namespace) {
139+
// TODO(FW-811): Ivy may cause issues here because it's passing around
140+
// full URIs for namespaces, therefore this lookup will fail.
135141
el.removeAttributeNS(NAMESPACE_URIS[namespace], name);
136142
} else {
137143
el.removeAttribute(name);

0 commit comments

Comments
 (0)