fix(ivy): set namespace for host elements of dynamically created components (#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 #35136
This commit is contained in:
parent
d5d9971c28
commit
ae0253f34a
|
@ -19,6 +19,7 @@ import {RendererFactory2} from '../render/api';
|
|||
import {Sanitizer} from '../sanitization/sanitizer';
|
||||
import {VERSION} from '../version';
|
||||
import {NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from '../view/provider';
|
||||
|
||||
import {assertComponentType} from './assert';
|
||||
import {LifecycleHooksFeature, createRootComponent, createRootComponentView, createRootContext} from './component';
|
||||
import {getComponentDef} from './definition';
|
||||
|
@ -28,6 +29,7 @@ import {ComponentDef} from './interfaces/definition';
|
|||
import {TContainerNode, TElementContainerNode, TElementNode} from './interfaces/node';
|
||||
import {RNode, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
|
||||
import {LView, LViewFlags, TVIEW, TViewType} from './interfaces/view';
|
||||
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces';
|
||||
import {stringifyCSSSelectorList} from './node_selector_matcher';
|
||||
import {enterView, leaveView} from './state';
|
||||
import {defaultScheduler} from './util/misc_utils';
|
||||
|
@ -59,6 +61,11 @@ function toRefArray(map: {[key: string]: string}): {propName: string; templateNa
|
|||
return array;
|
||||
}
|
||||
|
||||
function getNamespace(elementName: string): string|null {
|
||||
const name = elementName.toLowerCase();
|
||||
return name === 'svg' ? SVG_NAMESPACE : (name === 'math' ? MATH_ML_NAMESPACE : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* A change detection scheduler token for {@link RootContext}. This token is the default value used
|
||||
* for the default `RootContext` found in the {@link ROOT_CONTEXT} token.
|
||||
|
@ -132,14 +139,14 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
|
|||
const sanitizer = rootViewInjector.get(Sanitizer, null);
|
||||
|
||||
const hostRenderer = rendererFactory.createRenderer(null, this.componentDef);
|
||||
// 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 elementName = this.componentDef.selectors[0][0] as string || 'div';
|
||||
const hostRNode = rootSelectorOrNode ?
|
||||
locateHostElement(hostRenderer, rootSelectorOrNode, this.componentDef.encapsulation) :
|
||||
// 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.
|
||||
elementCreate(
|
||||
this.componentDef.selectors[0][0] as string || 'div',
|
||||
rendererFactory.createRenderer(null, this.componentDef), null);
|
||||
elementName, rendererFactory.createRenderer(null, this.componentDef),
|
||||
getNamespace(elementName));
|
||||
|
||||
const rootFlags = this.componentDef.onPush ? LViewFlags.Dirty | LViewFlags.IsRoot :
|
||||
LViewFlags.CheckAlways | LViewFlags.IsRoot;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
||||
export const MATH_ML_NAMESPACE = 'http://www.w3.org/1998/MathML/';
|
|
@ -11,6 +11,7 @@ import {assertDefined} from '../util/assert';
|
|||
import {assertLViewOrUndefined} from './assert';
|
||||
import {TNode} from './interfaces/node';
|
||||
import {CONTEXT, DECLARATION_VIEW, LView, OpaqueViewState, TVIEW, TView} from './interfaces/view';
|
||||
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from './namespaces';
|
||||
|
||||
|
||||
/**
|
||||
|
@ -511,7 +512,7 @@ export function setSelectedIndex(index: number) {
|
|||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵnamespaceSVG() {
|
||||
instructionState.lFrame.currentNamespace = 'http://www.w3.org/2000/svg';
|
||||
instructionState.lFrame.currentNamespace = SVG_NAMESPACE;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -520,7 +521,7 @@ export function ɵɵnamespaceSVG() {
|
|||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵnamespaceMathML() {
|
||||
instructionState.lFrame.currentNamespace = 'http://www.w3.org/1998/MathML/';
|
||||
instructionState.lFrame.currentNamespace = MATH_ML_NAMESPACE;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
|
||||
import {CommonModule, DOCUMENT} from '@angular/common';
|
||||
import {computeMsgId} from '@angular/compiler';
|
||||
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';
|
||||
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';
|
||||
import {Input} from '@angular/core/src/metadata';
|
||||
import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode';
|
||||
import {TestBed, TestComponentRenderer} from '@angular/core/testing';
|
||||
import {clearTranslations, loadTranslations} from '@angular/localize';
|
||||
import {By, DomSanitizer} from '@angular/platform-browser';
|
||||
import {By, DomSanitizer, ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
|
||||
|
||||
|
@ -160,6 +160,97 @@ describe('ViewContainerRef', () => {
|
|||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement.innerHTML).toContain('Hello');
|
||||
});
|
||||
|
||||
describe('element namespaces', () => {
|
||||
function runTestWithSelectors(svgSelector: string, mathMLSelector: string) {
|
||||
it('should be set correctly for host elements of dynamically created components', () => {
|
||||
@Component({
|
||||
selector: svgSelector,
|
||||
template: '<svg><g></g></svg>',
|
||||
})
|
||||
class SvgComp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: mathMLSelector,
|
||||
template: '<math><matrix></matrix></math>',
|
||||
})
|
||||
class MathMLComp {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
entryComponents: [SvgComp, MathMLComp],
|
||||
declarations: [SvgComp, MathMLComp],
|
||||
// View Engine doesn't have MathML tags listed in `DomElementSchemaRegistry`, thus
|
||||
// throwing "unknown element" error (':math:matrix' is not a known element). Ignore
|
||||
// these errors by adding `NO_ERRORS_SCHEMA` to this NgModule.
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
})
|
||||
class RootModule {
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ng-container #svg></ng-container>
|
||||
<ng-container #mathml></ng-container>
|
||||
`
|
||||
})
|
||||
class TestComp {
|
||||
@ViewChild('svg', {read: ViewContainerRef}) svgVCRef !: ViewContainerRef;
|
||||
@ViewChild('mathml', {read: ViewContainerRef}) mathMLVCRef !: ViewContainerRef;
|
||||
|
||||
constructor(public cfr: ComponentFactoryResolver) {}
|
||||
|
||||
createDynamicComponents() {
|
||||
const svgFactory = this.cfr.resolveComponentFactory(SvgComp);
|
||||
this.svgVCRef.createComponent(svgFactory);
|
||||
|
||||
const mathMLFactory = this.cfr.resolveComponentFactory(MathMLComp);
|
||||
this.mathMLVCRef.createComponent(mathMLFactory);
|
||||
}
|
||||
}
|
||||
|
||||
function _document(): any {
|
||||
// Tell Ivy about the global document
|
||||
ɵsetDocument(document);
|
||||
return document;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TestComp],
|
||||
imports: [RootModule],
|
||||
providers: [
|
||||
{provide: DOCUMENT, useFactory: _document, deps: []},
|
||||
// TODO(FW-811): switch back to default server renderer (i.e. remove the line below)
|
||||
// once it starts to support Ivy namespace format (URIs) correctly. For now, use
|
||||
// `DomRenderer` that supports Ivy namespace format.
|
||||
{provide: RendererFactory2, useClass: DomRendererFactory2}
|
||||
],
|
||||
});
|
||||
const fixture = TestBed.createComponent(TestComp);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.createDynamicComponents();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.querySelector('svg').namespaceURI)
|
||||
.toEqual('http://www.w3.org/2000/svg');
|
||||
|
||||
// View Engine doesn't set MathML namespace, since it's not present in the list of
|
||||
// known namespaces here:
|
||||
// https://github.com/angular/angular/blob/master/packages/platform-browser/src/dom/dom_renderer.ts#L14
|
||||
if (ivyEnabled) {
|
||||
expect(fixture.nativeElement.querySelector('math').namespaceURI)
|
||||
.toEqual('http://www.w3.org/1998/MathML/');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
runTestWithSelectors('svg[some-attr]', 'math[some-attr]');
|
||||
|
||||
// Also test with selector that has element name in uppercase
|
||||
runTestWithSelectors('SVG[some-attr]', 'MATH[some-attr]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('insert', () => {
|
||||
|
|
|
@ -174,7 +174,7 @@ class DefaultDomRenderer2 implements Renderer2 {
|
|||
setAttribute(el: any, name: string, value: string, namespace?: string): void {
|
||||
if (namespace) {
|
||||
name = namespace + ':' + name;
|
||||
// TODO(benlesh): Ivy may cause issues here because it's passing around
|
||||
// TODO(FW-811): Ivy may cause issues here because it's passing around
|
||||
// full URIs for namespaces, therefore this lookup will fail.
|
||||
const namespaceUri = NAMESPACE_URIS[namespace];
|
||||
if (namespaceUri) {
|
||||
|
@ -189,13 +189,13 @@ class DefaultDomRenderer2 implements Renderer2 {
|
|||
|
||||
removeAttribute(el: any, name: string, namespace?: string): void {
|
||||
if (namespace) {
|
||||
// TODO(benlesh): Ivy may cause issues here because it's passing around
|
||||
// TODO(FW-811): Ivy may cause issues here because it's passing around
|
||||
// full URIs for namespaces, therefore this lookup will fail.
|
||||
const namespaceUri = NAMESPACE_URIS[namespace];
|
||||
if (namespaceUri) {
|
||||
el.removeAttributeNS(namespaceUri, name);
|
||||
} else {
|
||||
// TODO(benlesh): Since ivy is passing around full URIs for namespaces
|
||||
// TODO(FW-811): Since ivy is passing around full URIs for namespaces
|
||||
// this could result in properties like `http://www.w3.org/2000/svg:cx="123"`,
|
||||
// which is wrong.
|
||||
el.removeAttribute(`${namespace}:${name}`);
|
||||
|
|
|
@ -73,6 +73,8 @@ class DefaultServerRenderer2 implements Renderer2 {
|
|||
createElement(name: string, namespace?: string, debugInfo?: any): any {
|
||||
if (namespace) {
|
||||
const doc = this.document || getDOM().getDefaultDocument();
|
||||
// TODO(FW-811): Ivy may cause issues here because it's passing around
|
||||
// full URIs for namespaces, therefore this lookup will fail.
|
||||
return doc.createElementNS(NAMESPACE_URIS[namespace], name);
|
||||
}
|
||||
|
||||
|
@ -124,6 +126,8 @@ class DefaultServerRenderer2 implements Renderer2 {
|
|||
|
||||
setAttribute(el: any, name: string, value: string, namespace?: string): void {
|
||||
if (namespace) {
|
||||
// TODO(FW-811): Ivy may cause issues here because it's passing around
|
||||
// full URIs for namespaces, therefore this lookup will fail.
|
||||
el.setAttributeNS(NAMESPACE_URIS[namespace], namespace + ':' + name, value);
|
||||
} else {
|
||||
el.setAttribute(name, value);
|
||||
|
@ -132,6 +136,8 @@ class DefaultServerRenderer2 implements Renderer2 {
|
|||
|
||||
removeAttribute(el: any, name: string, namespace?: string): void {
|
||||
if (namespace) {
|
||||
// TODO(FW-811): Ivy may cause issues here because it's passing around
|
||||
// full URIs for namespaces, therefore this lookup will fail.
|
||||
el.removeAttributeNS(NAMESPACE_URIS[namespace], name);
|
||||
} else {
|
||||
el.removeAttribute(name);
|
||||
|
|
Loading…
Reference in New Issue