fix(platform-browser): setAttribute should work with xmlns namespace (#14874)

Closes #14865
This commit is contained in:
Dzmitry Shylovich 2017-03-23 22:52:06 +03:00 committed by Victor Berchet
parent 08f2f08d74
commit 92084f2b6a
2 changed files with 93 additions and 28 deletions

View File

@ -6,16 +6,17 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {APP_ID, Inject, Injectable, RenderComponentType, Renderer, Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2, RootRenderer, ViewEncapsulation} from '@angular/core'; import {Injectable, Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2, ViewEncapsulation} from '@angular/core';
import {EventManager} from './events/event_manager'; import {EventManager} from './events/event_manager';
import {DomSharedStylesHost} from './shared_styles_host'; import {DomSharedStylesHost} from './shared_styles_host';
export const NAMESPACE_URIS: {[ns: string]: string} = { export const NAMESPACE_URIS: {[ns: string]: string} = {
'xlink': 'http://www.w3.org/1999/xlink',
'svg': 'http://www.w3.org/2000/svg', 'svg': 'http://www.w3.org/2000/svg',
'xhtml': 'http://www.w3.org/1999/xhtml', 'xhtml': 'http://www.w3.org/1999/xhtml',
'xml': 'http://www.w3.org/XML/1998/namespace' 'xlink': 'http://www.w3.org/1999/xlink',
'xml': 'http://www.w3.org/XML/1998/namespace',
'xmlns': 'http://www.w3.org/2000/xmlns/',
}; };
const COMPONENT_REGEX = /%COMP%/g; const COMPONENT_REGEX = /%COMP%/g;
@ -146,7 +147,13 @@ class DefaultDomRenderer2 implements Renderer2 {
setAttribute(el: any, name: string, value: string, namespace?: string): void { setAttribute(el: any, name: string, value: string, namespace?: string): void {
if (namespace) { if (namespace) {
el.setAttributeNS(NAMESPACE_URIS[namespace], namespace + ':' + name, value); name = `${namespace}:${name}`;
const namespaceUri = NAMESPACE_URIS[namespace];
if (namespaceUri) {
el.setAttributeNS(namespaceUri, name, value);
} else {
el.setAttribute(name, value);
}
} else { } else {
el.setAttribute(name, value); el.setAttribute(name, value);
} }
@ -154,7 +161,12 @@ class DefaultDomRenderer2 implements Renderer2 {
removeAttribute(el: any, name: string, namespace?: string): void { removeAttribute(el: any, name: string, namespace?: string): void {
if (namespace) { if (namespace) {
el.removeAttributeNS(NAMESPACE_URIS[namespace], name); const namespaceUri = NAMESPACE_URIS[namespace];
if (namespaceUri) {
el.removeAttributeNS(namespaceUri, name);
} else {
el.removeAttribute(`${namespace}:${name}`);
}
} else { } else {
el.removeAttribute(name); el.removeAttribute(name);
} }

View File

@ -5,30 +5,90 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {CommonModule} from '@angular/common'; import {Component, Renderer2, ViewEncapsulation} from '@angular/core';
import {Component, NgModule, ViewEncapsulation} from '@angular/core';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {By} from '@angular/platform-browser/src/dom/debug/by'; import {By} from '@angular/platform-browser/src/dom/debug/by';
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util'; import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
import {NAMESPACE_URIS} from '../../src/dom/dom_renderer';
export function main() { export function main() {
describe('DomRenderer', () => { describe('DefaultDomRendererV2', () => {
let renderer: Renderer2;
beforeEach(() => TestBed.configureTestingModule({imports: [BrowserModule, TestModule]})); beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
TestCmp, SomeApp, CmpEncapsulationEmulated, CmpEncapsulationNative, CmpEncapsulationNone
]
});
renderer = TestBed.createComponent(TestCmp).componentInstance.renderer;
});
describe('setAttribute', () => {
describe('with namespace', () => {
it('xmlns', () => shouldSetAttributeWithNs('xmlns'));
it('xml', () => shouldSetAttributeWithNs('xml'));
it('svg', () => shouldSetAttributeWithNs('svg'));
it('xhtml', () => shouldSetAttributeWithNs('xhtml'));
it('xlink', () => shouldSetAttributeWithNs('xlink'));
it('unknown', () => {
const div = document.createElement('div');
expect(div.hasAttribute('unknown:name')).toBe(false);
renderer.setAttribute(div, 'name', 'value', 'unknown');
expect(div.getAttribute('unknown:name')).toBe('value');
});
function shouldSetAttributeWithNs(namespace: string): void {
const namespaceUri = NAMESPACE_URIS[namespace];
const div = document.createElement('div');
expect(div.hasAttributeNS(namespaceUri, 'name')).toBe(false);
renderer.setAttribute(div, 'name', 'value', namespace);
expect(div.getAttributeNS(namespaceUri, 'name')).toBe('value');
}
});
});
describe('removeAttribute', () => {
describe('with namespace', () => {
it('xmlns', () => shouldRemoveAttributeWithNs('xmlns'));
it('xml', () => shouldRemoveAttributeWithNs('xml'));
it('svg', () => shouldRemoveAttributeWithNs('svg'));
it('xhtml', () => shouldRemoveAttributeWithNs('xhtml'));
it('xlink', () => shouldRemoveAttributeWithNs('xlink'));
it('unknown', () => {
const div = document.createElement('div');
div.setAttribute('unknown:name', 'value');
expect(div.hasAttribute('unknown:name')).toBe(true);
renderer.removeAttribute(div, 'name', 'unknown');
expect(div.hasAttribute('unknown:name')).toBe(false);
});
function shouldRemoveAttributeWithNs(namespace: string): void {
const namespaceUri = NAMESPACE_URIS[namespace];
const div = document.createElement('div');
div.setAttributeNS(namespaceUri, `${namespace}:name`, 'value');
expect(div.hasAttributeNS(namespaceUri, 'name')).toBe(true);
renderer.removeAttribute(div, 'name', namespace);
expect(div.hasAttributeNS(namespaceUri, 'name')).toBe(false);
}
});
});
// other browsers don't support shadow dom // other browsers don't support shadow dom
if (browserDetection.isChromeDesktop) { if (browserDetection.isChromeDesktop) {
it('should allow to style components with emulated encapsulation and no encapsulation inside of components with shadow DOM', it('should allow to style components with emulated encapsulation and no encapsulation inside of components with shadow DOM',
() => { () => {
TestBed.overrideComponent(CmpEncapsulationNative, {
set: {
template:
'<div class="native"></div><cmp-emulated></cmp-emulated><cmp-none></cmp-none>'
}
});
const fixture = TestBed.createComponent(SomeApp); const fixture = TestBed.createComponent(SomeApp);
const cmp = fixture.debugElement.query(By.css('cmp-native')).nativeElement; const cmp = fixture.debugElement.query(By.css('cmp-native')).nativeElement;
@ -49,7 +109,7 @@ export function main() {
@Component({ @Component({
selector: 'cmp-native', selector: 'cmp-native',
template: `<div class="native"></div>`, template: `<div class="native"></div><cmp-emulated></cmp-emulated><cmp-none></cmp-none>`,
styles: [`.native { color: red; }`], styles: [`.native { color: red; }`],
encapsulation: ViewEncapsulation.Native encapsulation: ViewEncapsulation.Native
}) })
@ -85,14 +145,7 @@ class CmpEncapsulationNone {
export class SomeApp { export class SomeApp {
} }
@NgModule({ @Component({selector: 'test-cmp', template: ''})
declarations: [ class TestCmp {
SomeApp, constructor(public renderer: Renderer2) {}
CmpEncapsulationNative,
CmpEncapsulationEmulated,
CmpEncapsulationNone,
],
imports: [CommonModule]
})
class TestModule {
} }