feat(core): add support for ShadowDOM v1 (#24718)

add a new ViewEncapsulation.ShadowDom option that uses the v1 Shadow DOM API to provide style encapsulation.

PR Close #24718
This commit is contained in:
Rob Wormald 2018-06-28 12:42:04 -07:00 committed by Miško Hevery
parent 144a624088
commit 6c55a130b1
8 changed files with 198 additions and 8 deletions

View File

@ -280,12 +280,14 @@ To control how this encapsulation happens on a *per
component* basis, you can set the *view encapsulation mode* in the component metadata. component* basis, you can set the *view encapsulation mode* in the component metadata.
Choose from the following modes: Choose from the following modes:
* `Native` view encapsulation uses the browser's native shadow DOM implementation (see * `ShadowDom` view encapsulation uses the browser's native shadow DOM implementation (see
[Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM) [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM)
on the [MDN](https://developer.mozilla.org) site) on the [MDN](https://developer.mozilla.org) site)
to attach a shadow DOM to the component's host element, and then puts the component to attach a shadow DOM to the component's host element, and then puts the component
view inside that shadow DOM. The component's styles are included within the shadow DOM. view inside that shadow DOM. The component's styles are included within the shadow DOM.
* `Native` view encapsulation uses a now deprecated version of the browser's native shadow DOM implementation - [learn about the changes](https://hayato.io/2016/shadowdomv1/).
* `Emulated` view encapsulation (the default) emulates the behavior of shadow DOM by preprocessing * `Emulated` view encapsulation (the default) emulates the behavior of shadow DOM by preprocessing
(and renaming) the CSS code to effectively scope the CSS to the component's view. (and renaming) the CSS code to effectively scope the CSS to the component's view.
For details, see [Appendix 1](guide/component-styles#inspect-generated-css). For details, see [Appendix 1](guide/component-styles#inspect-generated-css).
@ -300,8 +302,8 @@ To set the components encapsulation mode, use the `encapsulation` property in th
<code-example path="component-styles/src/app/quest-summary.component.ts" region="encapsulation.native" title="src/app/quest-summary.component.ts" linenums="false"> <code-example path="component-styles/src/app/quest-summary.component.ts" region="encapsulation.native" title="src/app/quest-summary.component.ts" linenums="false">
</code-example> </code-example>
`Native` view encapsulation only works on browsers that have native support `ShadowDom` view encapsulation only works on browsers that have native support
for shadow DOM (see [Shadow DOM v0](http://caniuse.com/#feat=shadowdom) on the for shadow DOM (see [Shadow DOM v1](https://caniuse.com/#feat=shadowdomv1) on the
[Can I use](http://caniuse.com) site). The support is still limited, [Can I use](http://caniuse.com) site). The support is still limited,
which is why `Emulated` view encapsulation is the default mode and recommended which is why `Emulated` view encapsulation is the default mode and recommended
in most cases. in most cases.

View File

@ -73,7 +73,8 @@ export interface Component extends Directive {
export enum ViewEncapsulation { export enum ViewEncapsulation {
Emulated = 0, Emulated = 0,
Native = 1, Native = 1,
None = 2 None = 2,
ShadowDom = 3
} }
export enum ChangeDetectionStrategy { export enum ChangeDetectionStrategy {

View File

@ -23,14 +23,28 @@ export enum ViewEncapsulation {
*/ */
Emulated = 0, Emulated = 0,
/** /**
* @deprecated v6.1.0 - use {ViewEncapsulation.ShadowDom} instead.
* Use the native encapsulation mechanism of the renderer. * Use the native encapsulation mechanism of the renderer.
* *
* For the DOM this means using [Shadow DOM](https://w3c.github.io/webcomponents/spec/shadow/) and * For the DOM this means using the deprecated [Shadow DOM
* v0](https://w3c.github.io/webcomponents/spec/shadow/) and
* creating a ShadowRoot for Component's Host Element. * creating a ShadowRoot for Component's Host Element.
*/ */
Native = 1, Native = 1,
/** /**
* Don't provide any template or style encapsulation. * Don't provide any template or style encapsulation.
*/ */
None = 2 None = 2,
/**
* Use Shadow DOM to encapsulate styles.
*
* For the DOM this means using modern [Shadow
* DOM](https://w3c.github.io/webcomponents/spec/shadow/) and
* creating a ShadowRoot for Component's Host Element.
*
* ### Example
* {@example core/ts/metadata/encapsulation.ts region='longform'}
*/
ShadowDom = 3
} }

View File

@ -0,0 +1,35 @@
/**
* @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
*/
import {Component, ViewEncapsulation} from '@angular/core';
// #docregion longform
@Component({
selector: 'my-app',
template: `
<h1>Hello World!</h1>
<span class="red">Shadow DOM Rocks!</span>
`,
styles: [`
:host {
display: block;
border: 1px solid black;
}
h1 {
color: blue;
}
.red {
background-color: red;
}
`],
encapsulation: ViewEncapsulation.ShadowDom
})
class MyApp {
}
// #enddocregion

View File

@ -83,6 +83,7 @@ export class DomRendererFactory2 implements RendererFactory2 {
return renderer; return renderer;
} }
case ViewEncapsulation.Native: case ViewEncapsulation.Native:
case ViewEncapsulation.ShadowDom:
return new ShadowDomRenderer(this.eventManager, this.sharedStylesHost, element, type); return new ShadowDomRenderer(this.eventManager, this.sharedStylesHost, element, type);
default: { default: {
if (!this.rendererByCompId.has(type.id)) { if (!this.rendererByCompId.has(type.id)) {
@ -256,7 +257,11 @@ class ShadowDomRenderer extends DefaultDomRenderer2 {
eventManager: EventManager, private sharedStylesHost: DomSharedStylesHost, eventManager: EventManager, private sharedStylesHost: DomSharedStylesHost,
private hostEl: any, private component: RendererType2) { private hostEl: any, private component: RendererType2) {
super(eventManager); super(eventManager);
this.shadowRoot = (hostEl as any).createShadowRoot(); if (component.encapsulation === ViewEncapsulation.ShadowDom) {
this.shadowRoot = (hostEl as any).attachShadow({mode: 'open'});
} else {
this.shadowRoot = (hostEl as any).createShadowRoot();
}
this.sharedStylesHost.addHost(this.shadowRoot); this.sharedStylesHost.addHost(this.shadowRoot);
const styles = flattenStyles(component.id, component.styles, []); const styles = flattenStyles(component.id, component.styles, []);
for (let i = 0; i < styles.length; i++) { for (let i = 0; i < styles.length; i++) {

View File

@ -20,7 +20,8 @@ import {NAMESPACE_URIS} from '../../src/dom/dom_renderer';
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ declarations: [
TestCmp, SomeApp, CmpEncapsulationEmulated, CmpEncapsulationNative, CmpEncapsulationNone TestCmp, SomeApp, CmpEncapsulationEmulated, CmpEncapsulationNative, CmpEncapsulationNone,
CmpEncapsulationNative
] ]
}); });
renderer = TestBed.createComponent(TestCmp).componentInstance.renderer; renderer = TestBed.createComponent(TestCmp).componentInstance.renderer;
@ -135,6 +136,15 @@ class CmpEncapsulationEmulated {
class CmpEncapsulationNone { class CmpEncapsulationNone {
} }
@Component({
selector: 'cmp-shadow',
template: `<div class="shadow"></div><cmp-emulated></cmp-emulated><cmp-none></cmp-none>`,
styles: [`.native { color: red; }`],
encapsulation: ViewEncapsulation.ShadowDom
})
class CmpEncapsulationShadow {
}
@Component({ @Component({
selector: 'some-app', selector: 'some-app',
template: ` template: `

View File

@ -0,0 +1,122 @@
/**
* @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
*/
import {Component, EventEmitter, Injector, Input, NgModule, Output, Renderer2, ViewEncapsulation, destroyPlatform} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {expect} from '@angular/platform-browser/testing/src/matchers';
function supportsShadowDOMV1() {
const testEl = document.createElement('div');
return (typeof customElements !== 'undefined') && (typeof testEl.attachShadow !== 'undefined');
}
if (supportsShadowDOMV1()) {
describe('ShadowDOM Support', () => {
let testContainer: HTMLDivElement;
beforeEach(() => { TestBed.configureTestingModule({imports: [TestModule]}); });
it('should attach and use a shadowRoot when ViewEncapsulation.Native is set', () => {
const compEl = TestBed.createComponent(ShadowComponent).nativeElement;
expect(compEl.shadowRoot !.textContent).toEqual('Hello World');
});
it('should use the shadow root to encapsulate styles', () => {
const compEl = TestBed.createComponent(StyledShadowComponent).nativeElement;
expect(window.getComputedStyle(compEl).border).toEqual('1px solid rgb(0, 0, 0)');
const redDiv = compEl.shadowRoot.querySelector('div.red');
expect(window.getComputedStyle(redDiv).border).toEqual('1px solid rgb(255, 0, 0)');
});
it('should allow the usage of <slot> elements', () => {
const el = TestBed.createComponent(ShadowSlotComponent).nativeElement;
const projectedContent = document.createTextNode('Hello Slot!');
el.appendChild(projectedContent);
const slot = el.shadowRoot !.querySelector('slot');
expect(slot !.assignedNodes().length).toBe(1);
expect(slot !.assignedNodes()[0].textContent).toBe('Hello Slot!');
});
it('should allow the usage of named <slot> elements', () => {
const el = TestBed.createComponent(ShadowSlotsComponent).nativeElement;
const headerContent = document.createElement('h1');
headerContent.setAttribute('slot', 'header');
headerContent.textContent = 'Header Text!';
const articleContent = document.createElement('span');
articleContent.setAttribute('slot', 'article');
articleContent.textContent = 'Article Text!';
const articleSubcontent = document.createElement('span');
articleSubcontent.setAttribute('slot', 'article');
articleSubcontent.textContent = 'Article Subtext!';
el.appendChild(headerContent);
el.appendChild(articleContent);
el.appendChild(articleSubcontent);
const headerSlot = el.shadowRoot !.querySelector('slot[name=header]') as HTMLSlotElement;
const articleSlot = el.shadowRoot !.querySelector('slot[name=article]') as HTMLSlotElement;
expect(headerSlot !.assignedNodes().length).toBe(1);
expect(headerSlot !.assignedNodes()[0].textContent).toBe('Header Text!');
expect(headerContent.assignedSlot).toBe(headerSlot);
expect(articleSlot !.assignedNodes().length).toBe(2);
expect(articleSlot !.assignedNodes()[0].textContent).toBe('Article Text!');
expect(articleSlot !.assignedNodes()[1].textContent).toBe('Article Subtext!');
expect(articleContent.assignedSlot).toBe(articleSlot);
expect(articleSubcontent.assignedSlot).toBe(articleSlot);
});
});
}
@Component(
{selector: 'shadow-comp', template: 'Hello World', encapsulation: ViewEncapsulation.ShadowDom})
class ShadowComponent {
}
@Component({
selector: 'styled-shadow-comp',
template: '<div class="red"></div>',
encapsulation: ViewEncapsulation.ShadowDom,
styles: [`:host { border: 1px solid black; } .red { border: 1px solid red; }`]
})
class StyledShadowComponent {
}
@Component({
selector: 'shadow-slot-comp',
template: '<slot></slot>',
encapsulation: ViewEncapsulation.ShadowDom
})
class ShadowSlotComponent {
}
@Component({
selector: 'shadow-slots-comp',
template:
'<header><slot name="header"></slot></header><article><slot name="article"></slot></article>',
encapsulation: ViewEncapsulation.ShadowDom
})
class ShadowSlotsComponent {
}
@NgModule({
imports: [BrowserModule],
declarations: [ShadowComponent, ShadowSlotComponent, ShadowSlotsComponent, StyledShadowComponent],
entryComponents:
[ShadowComponent, ShadowSlotComponent, ShadowSlotsComponent, StyledShadowComponent],
})
class TestModule {
ngDoBootstrap() {}
}

View File

@ -932,6 +932,7 @@ export declare enum ViewEncapsulation {
Emulated = 0, Emulated = 0,
Native = 1, Native = 1,
None = 2, None = 2,
ShadowDom = 3,
} }
export declare abstract class ViewRef extends ChangeDetectorRef { export declare abstract class ViewRef extends ChangeDetectorRef {