diff --git a/packages/platform-browser/src/dom/dom_renderer.ts b/packages/platform-browser/src/dom/dom_renderer.ts index ddcb799c79..91b295982a 100644 --- a/packages/platform-browser/src/dom/dom_renderer.ts +++ b/packages/platform-browser/src/dom/dom_renderer.ts @@ -6,16 +6,17 @@ * 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 {DomSharedStylesHost} from './shared_styles_host'; export const NAMESPACE_URIS: {[ns: string]: string} = { - 'xlink': 'http://www.w3.org/1999/xlink', 'svg': 'http://www.w3.org/2000/svg', '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; @@ -146,7 +147,13 @@ class DefaultDomRenderer2 implements Renderer2 { setAttribute(el: any, name: string, value: string, namespace?: string): void { 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 { el.setAttribute(name, value); } @@ -154,7 +161,12 @@ class DefaultDomRenderer2 implements Renderer2 { removeAttribute(el: any, name: string, namespace?: string): void { 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 { el.removeAttribute(name); } diff --git a/packages/platform-browser/test/dom/dom_renderer_spec.ts b/packages/platform-browser/test/dom/dom_renderer_spec.ts index 448bea205d..e3332ad818 100644 --- a/packages/platform-browser/test/dom/dom_renderer_spec.ts +++ b/packages/platform-browser/test/dom/dom_renderer_spec.ts @@ -5,30 +5,90 @@ * 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 */ -import {CommonModule} from '@angular/common'; -import {Component, NgModule, ViewEncapsulation} from '@angular/core'; +import {Component, Renderer2, ViewEncapsulation} from '@angular/core'; import {TestBed} from '@angular/core/testing'; -import {BrowserModule} from '@angular/platform-browser'; import {By} from '@angular/platform-browser/src/dom/debug/by'; import {browserDetection} from '@angular/platform-browser/testing/src/browser_util'; import {expect} from '@angular/platform-browser/testing/src/matchers'; +import {NAMESPACE_URIS} from '../../src/dom/dom_renderer'; 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 if (browserDetection.isChromeDesktop) { it('should allow to style components with emulated encapsulation and no encapsulation inside of components with shadow DOM', () => { - TestBed.overrideComponent(CmpEncapsulationNative, { - set: { - template: - '
' - } - }); - const fixture = TestBed.createComponent(SomeApp); const cmp = fixture.debugElement.query(By.css('cmp-native')).nativeElement; @@ -49,7 +109,7 @@ export function main() { @Component({ selector: 'cmp-native', - template: `
`, + template: `
`, styles: [`.native { color: red; }`], encapsulation: ViewEncapsulation.Native }) @@ -85,14 +145,7 @@ class CmpEncapsulationNone { export class SomeApp { } -@NgModule({ - declarations: [ - SomeApp, - CmpEncapsulationNative, - CmpEncapsulationEmulated, - CmpEncapsulationNone, - ], - imports: [CommonModule] -}) -class TestModule { +@Component({selector: 'test-cmp', template: ''}) +class TestCmp { + constructor(public renderer: Renderer2) {} }