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:
parent
1ae3f87383
commit
3553977bd7
|
@ -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.
|
||||
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)
|
||||
on the [MDN](https://developer.mozilla.org) site)
|
||||
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.
|
||||
|
||||
* `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
|
||||
(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).
|
||||
|
@ -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>
|
||||
|
||||
`Native` view encapsulation only works on browsers that have native support
|
||||
for shadow DOM (see [Shadow DOM v0](http://caniuse.com/#feat=shadowdom) on the
|
||||
`ShadowDom` view encapsulation only works on browsers that have native support
|
||||
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,
|
||||
which is why `Emulated` view encapsulation is the default mode and recommended
|
||||
in most cases.
|
||||
|
|
|
@ -75,7 +75,8 @@ export interface Component extends Directive {
|
|||
export enum ViewEncapsulation {
|
||||
Emulated = 0,
|
||||
Native = 1,
|
||||
None = 2
|
||||
None = 2,
|
||||
ShadowDom = 3
|
||||
}
|
||||
|
||||
export enum ChangeDetectionStrategy {
|
||||
|
|
|
@ -23,14 +23,28 @@ export enum ViewEncapsulation {
|
|||
*/
|
||||
Emulated = 0,
|
||||
/**
|
||||
* @deprecated v6.1.0 - use {ViewEncapsulation.ShadowDom} instead.
|
||||
* 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.
|
||||
*/
|
||||
Native = 1,
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -83,6 +83,7 @@ export class DomRendererFactory2 implements RendererFactory2 {
|
|||
return renderer;
|
||||
}
|
||||
case ViewEncapsulation.Native:
|
||||
case ViewEncapsulation.ShadowDom:
|
||||
return new ShadowDomRenderer(this.eventManager, this.sharedStylesHost, element, type);
|
||||
default: {
|
||||
if (!this.rendererByCompId.has(type.id)) {
|
||||
|
@ -256,7 +257,11 @@ class ShadowDomRenderer extends DefaultDomRenderer2 {
|
|||
eventManager: EventManager, private sharedStylesHost: DomSharedStylesHost,
|
||||
private hostEl: any, private component: RendererType2) {
|
||||
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);
|
||||
const styles = flattenStyles(component.id, component.styles, []);
|
||||
for (let i = 0; i < styles.length; i++) {
|
||||
|
|
|
@ -20,7 +20,8 @@ import {NAMESPACE_URIS} from '../../src/dom/dom_renderer';
|
|||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
TestCmp, SomeApp, CmpEncapsulationEmulated, CmpEncapsulationNative, CmpEncapsulationNone
|
||||
TestCmp, SomeApp, CmpEncapsulationEmulated, CmpEncapsulationNative, CmpEncapsulationNone,
|
||||
CmpEncapsulationNative
|
||||
]
|
||||
});
|
||||
renderer = TestBed.createComponent(TestCmp).componentInstance.renderer;
|
||||
|
@ -135,6 +136,15 @@ class CmpEncapsulationEmulated {
|
|||
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({
|
||||
selector: 'some-app',
|
||||
template: `
|
||||
|
|
|
@ -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() {}
|
||||
}
|
|
@ -938,6 +938,7 @@ export declare enum ViewEncapsulation {
|
|||
Emulated = 0,
|
||||
Native = 1,
|
||||
None = 2,
|
||||
ShadowDom = 3,
|
||||
}
|
||||
|
||||
export declare abstract class ViewRef extends ChangeDetectorRef {
|
||||
|
|
Loading…
Reference in New Issue