Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import { Component, ViewEncapsulation } from '@angular/core';
export class QuestSummaryComponent { }
// #enddocregion
/*
// #docregion encapsulation.native
// #docregion encapsulation.shadowdom
// warning: few browsers support shadow DOM encapsulation at this time
encapsulation: ViewEncapsulation.Native
// #enddocregion encapsulation.native
encapsulation: ViewEncapsulation.ShadowDom
// #enddocregion encapsulation.shadowdom
*/
24 changes: 15 additions & 9 deletions aio/content/guide/component-styles.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,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
[Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM)
* `Native` view encapsulation uses the browser's native shadow DOM v0 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.

* `ShadowDom` view encapsulation uses the browser's native shadow DOM v1 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.
Expand All @@ -289,22 +295,22 @@ Choose from the following modes:
(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).

* `None` means that Angular does no view encapsulation.
Angular adds the CSS to the global styles.
The scoping rules, isolations, and protections discussed earlier don't apply.
* `None` means that Angular does no view encapsulation.
Angular adds the CSS to the global styles.
The scoping rules, isolations, and protections discussed earlier don't apply.
This is essentially the same as pasting the component's styles into the HTML.

To set the components encapsulation mode, use the `encapsulation` property in the component metadata:


<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.shadowdom" 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.
Expand Down
2 changes: 1 addition & 1 deletion aio/src/app/layout/doc-viewer/doc-viewer.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const initialDocViewerContent = initialDocViewerElement ? initialDocViewerElemen
selector: 'aio-doc-viewer',
template: ''
// TODO(robwormald): shadow DOM and emulated don't work here (?!)
// encapsulation: ViewEncapsulation.Native
// encapsulation: ViewEncapsulation.ShadowDom
})
export class DocViewerComponent implements DoCheck, OnDestroy {

Expand Down
3 changes: 2 additions & 1 deletion packages/compiler/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ export interface Component extends Directive {
export enum ViewEncapsulation {
Emulated = 0,
Native = 1,
None = 2
None = 2,
ShadowDom = 3
}

export enum ChangeDetectionStrategy {
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/metadata/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,9 @@ export interface Component extends Directive {
* - {@link ViewEncapsulation#Emulated `ViewEncapsulation.Emulated`} to use shimmed CSS that
* emulates the native behavior,
* - {@link ViewEncapsulation#None `ViewEncapsulation.None`} to use global CSS without any
* encapsulation.
* encapsulation,
* - {@link ViewEncapsulation#ShadowDom `ViewEncapsulation.ShadowDom`} to use shadow roots - only
* works if natively available on the platform.
*
* When no `encapsulation` is defined for the component, the default value from the
* {@link CompilerOptions} is used. The default is `ViewEncapsulation.Emulated`}. Provide a new
Expand Down
13 changes: 10 additions & 3 deletions packages/core/src/metadata/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,19 @@ export enum ViewEncapsulation {
/**
* Use the native encapsulation mechanism of the renderer.
*
* For the DOM this means using [Shadow DOM](https://w3c.github.io/webcomponents/spec/shadow/) and
* creating a ShadowRoot for Component's Host Element.
* For the DOM this means using [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 the ShadowDOM encapsulation mechanism of the renderer.
*
* For the DOM this means using [Shadow DOM v1](https://w3c.github.io/webcomponents/spec/shadow/)
* and creating a ShadowRoot for Component's Host Element.
*/
ShadowDom = 3
}
5 changes: 4 additions & 1 deletion packages/core/src/view/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,10 @@ export function getParentRenderElement(view: ViewData, renderHost: any, def: Nod
(renderParent.flags & NodeFlags.ComponentView) === 0 ||
(renderParent.element !.componentRendererType &&
renderParent.element !.componentRendererType !.encapsulation ===
ViewEncapsulation.Native)) {
ViewEncapsulation.Native) ||
(renderParent.element !.componentRendererType &&
renderParent.element !.componentRendererType !.encapsulation ===
ViewEncapsulation.ShadowDom)) {
// only children of non components, or children of components with native encapsulation should
// be attached.
return asElementData(view, def.renderParent !.index).renderElement;
Expand Down
37 changes: 36 additions & 1 deletion packages/core/test/linker/projection_integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ export function main() {
expect(main.nativeElement).toHaveText('TREE(0:TREE2(1:TREE(2:)))');
});

if (getDOM().supportsNativeShadowDOM()) {
if (getDOM().supportsNativeShadowDom()) {
it('should support native content projection and isolate styles per component', () => {
TestBed.configureTestingModule({declarations: [SimpleNative1, SimpleNative2]});
TestBed.overrideComponent(MainComp, {
Expand All @@ -385,6 +385,24 @@ export function main() {
});
}

if (getDOM().supportsShadowDom()) {
it('should support native content projection and isolate styles per component', () => {
TestBed.configureTestingModule({declarations: [SimpleShadowDom1, SimpleShadowDom2]});
TestBed.overrideComponent(MainComp, {
set: {
template: '<simple-shadow-dom1><div>A</div></simple-shadow-dom1>' +
'<simple-shadow-dom2><div>B</div></simple-shadow-dom2>'
}
});
const main = TestBed.createComponent(MainComp);

const childNodes = getDOM().childNodes(main.nativeElement);
expect(childNodes[0]).toHaveText('div {color: red}SIMPLE1(A)');
expect(childNodes[1]).toHaveText('div {color: blue}SIMPLE2(B)');
main.destroy();
});
}

if (getDOM().supportsDOMEvents()) {
it('should support non emulated styles', () => {
TestBed.configureTestingModule({declarations: [OtherComp]});
Expand Down Expand Up @@ -551,6 +569,23 @@ class SimpleNative1 {
})
class SimpleNative2 {
}
@Component({
selector: 'simple-shadow-dom1',
template: 'SIMPLE1(<slot></slot>)',
encapsulation: ViewEncapsulation.ShadowDom,
styles: ['div {color: red}']
})
class SimpleShadowDom1 {
}

@Component({
selector: 'simple-shadow-dom2',
template: 'SIMPLE2(<slot></slot>)',
encapsulation: ViewEncapsulation.ShadowDom,
styles: ['div {color: blue}']
})
class SimpleShadowDom2 {
}

@Component({selector: 'empty', template: ''})
class Empty {
Expand Down
1 change: 1 addition & 0 deletions packages/platform-browser/src/browser/browser_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
this.appendChild(style, this.createTextNode(css, doc));
return style;
}
attachShadow(el: HTMLElement): DocumentFragment { return (<any>el).attachShadow({mode: 'open'}); }
createShadowRoot(el: HTMLElement): DocumentFragment { return (<any>el).createShadowRoot(); }
getShadowRoot(el: HTMLElement): DocumentFragment { return (<any>el).shadowRoot; }
getHost(el: HTMLElement): HTMLElement { return (<any>el).host; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,16 @@ export abstract class GenericBrowserDomAdapter extends DomAdapter {
}
}

assignedNodes(el: HTMLElement): Node[] { return (<any>el).assignedNodes(); }
getDistributedNodes(el: HTMLElement): Node[] { return (<any>el).getDistributedNodes(); }
resolveAndSetHref(el: HTMLAnchorElement, baseUrl: string, href: string) {
el.href = href == null ? baseUrl : baseUrl + '/../' + href;
}
supportsDOMEvents(): boolean { return true; }
supportsNativeShadowDOM(): boolean {
supportsNativeShadowDom(): boolean {
return typeof(<any>document.body).createShadowRoot === 'function';
}
supportsShadowDom(): boolean { return typeof(<any>document.body).attachShadow === 'function'; }
getAnimationPrefix(): string { return this._animationPrefix ? this._animationPrefix : ''; }
getTransitionEnd(): string { return this._transitionEnd ? this._transitionEnd : ''; }
supportsAnimation(): boolean {
Expand Down
5 changes: 4 additions & 1 deletion packages/platform-browser/src/dom/dom_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,11 @@ export abstract class DomAdapter {
abstract createTextNode(text: string, doc?: any): Text;
abstract createScriptTag(attrName: string, attrValue: string, doc?: any): HTMLElement;
abstract createStyleElement(css: string, doc?: any): HTMLStyleElement;
abstract attachShadow(el: any): any;
abstract createShadowRoot(el: any): any;
abstract getShadowRoot(el: any): any;
abstract getHost(el: any): any;
abstract assignedNodes(el: any): Node[];
abstract getDistributedNodes(el: any): Node[];
abstract clone /*<T extends Node>*/ (node: Node /*T*/): Node /*T*/;
abstract getElementsByClassName(element: any, name: string): HTMLElement[];
Expand Down Expand Up @@ -142,7 +144,8 @@ export abstract class DomAdapter {
abstract getEventKey(event: any): string;
abstract resolveAndSetHref(element: any, baseUrl: string, href: string): any;
abstract supportsDOMEvents(): boolean;
abstract supportsNativeShadowDOM(): boolean;
abstract supportsNativeShadowDom(): boolean;
abstract supportsShadowDom(): boolean;
abstract getGlobalEventTarget(doc: Document, target: string): any;
abstract getHistory(): History;
abstract getLocation(): Location;
Expand Down
37 changes: 37 additions & 0 deletions packages/platform-browser/src/dom/dom_renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ export class DomRendererFactory2 implements RendererFactory2 {
}
case ViewEncapsulation.Native:
return new ShadowDomRenderer(this.eventManager, this.sharedStylesHost, element, type);
case ViewEncapsulation.ShadowDom:
return new ShadowDomRenderer1(this.eventManager, this.sharedStylesHost, element, type);
default: {
if (!this.rendererByCompId.has(type.id)) {
const styles = flattenStyles(type.id, type.styles, []);
Expand Down Expand Up @@ -283,3 +285,38 @@ class ShadowDomRenderer extends DefaultDomRenderer2 {
return this.nodeOrShadowRoot(super.parentNode(this.nodeOrShadowRoot(node)));
}
}

class ShadowDomRenderer1 extends DefaultDomRenderer2 {
private shadowRoot: any;

constructor(
eventManager: EventManager, private sharedStylesHost: DomSharedStylesHost,
private hostEl: any, private component: RendererType2) {
super(eventManager);
this.shadowRoot = (hostEl as any).attachShadow({mode: 'open'});
this.sharedStylesHost.addHost(this.shadowRoot);
const styles = flattenStyles(component.id, component.styles, []);
for (let i = 0; i < styles.length; i++) {
const styleEl = document.createElement('style');
styleEl.textContent = styles[i];
this.shadowRoot.appendChild(styleEl);
}
}

private nodeOrShadowRoot(node: any): any { return node === this.hostEl ? this.shadowRoot : node; }

destroy() { this.sharedStylesHost.removeHost(this.shadowRoot); }

appendChild(parent: any, newChild: any): void {
return super.appendChild(this.nodeOrShadowRoot(parent), newChild);
}
insertBefore(parent: any, newChild: any, refChild: any): void {
return super.insertBefore(this.nodeOrShadowRoot(parent), newChild, refChild);
}
removeChild(parent: any, oldChild: any): void {
return super.removeChild(this.nodeOrShadowRoot(parent), oldChild);
}
parentNode(node: any): any {
return this.nodeOrShadowRoot(super.parentNode(this.nodeOrShadowRoot(node)));
}
}
4 changes: 4 additions & 0 deletions packages/platform-browser/testing/src/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@ function elementText(n: any): string {
return elementText(Array.prototype.slice.apply(getDOM().getDistributedNodes(n)));
}

if (getDOM().isElementNode(n) && getDOM().tagName(n) == 'SLOT') {
return elementText(Array.prototype.slice.apply(getDOM().assignedNodes(n)));
}

if (getDOM().hasShadowRoot(n)) {
return elementText(getDOM().childNodesAsList(getDOM().getShadowRoot(n)));
}
Expand Down
12 changes: 10 additions & 2 deletions packages/platform-server/src/domino_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ export class DominoAdapter extends BrowserDomAdapter {
logGroupEnd() {}

supportsDOMEvents(): boolean { return false; }
supportsNativeShadowDOM(): boolean { return false; }
supportsNativeShadowDom(): boolean { return false; }
supportsShadowDom(): boolean { return false; }

contains(nodeA: any, nodeB: any): boolean {
let inner = nodeB;
Expand All @@ -71,6 +72,12 @@ export class DominoAdapter extends BrowserDomAdapter {
return DominoAdapter.defaultDoc;
}


attachShadow(el: any, doc: Document = document): DocumentFragment {
el.shadowRoot = doc.createDocumentFragment();
el.shadowRoot.parent = el;
return el.shadowRoot;
}
createShadowRoot(el: any, doc: Document = document): DocumentFragment {
el.shadowRoot = doc.createDocumentFragment();
el.shadowRoot.parent = el;
Expand Down Expand Up @@ -201,9 +208,10 @@ export class DominoAdapter extends BrowserDomAdapter {
getTransitionEnd(): string { return 'transitionend'; }
supportsAnimation(): boolean { return true; }

assignedNodes(el: any): Node[] { throw _notImplemented('assignedNodes'); }
getDistributedNodes(el: any): Node[] { throw _notImplemented('getDistributedNodes'); }

supportsCookies(): boolean { return false; }
getCookie(name: string): string { throw _notImplemented('getCookie'); }
setCookie(name: string, value: string) { throw _notImplemented('setCookie'); }
}
}
3 changes: 3 additions & 0 deletions packages/platform-server/src/server_renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class ServerRendererFactory2 implements RendererFactory2 {
return this.defaultRenderer;
}
switch (type.encapsulation) {
case ViewEncapsulation.ShadowDom:
case ViewEncapsulation.Native:
case ViewEncapsulation.Emulated: {
let renderer = this.rendererByCompId.get(type.id);
Expand All @@ -42,6 +43,8 @@ export class ServerRendererFactory2 implements RendererFactory2 {
}
case ViewEncapsulation.Native:
throw new Error('Native encapsulation is not supported on the server!');
case ViewEncapsulation.ShadowDom:
throw new Error('ShadowDom encapsulation is not supported on the server!');
default: {
if (!this.rendererByCompId.has(type.id)) {
const styles = flattenStyles(type.id, type.styles, []);
Expand Down
25 changes: 25 additions & 0 deletions packages/platform-server/test/integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,23 @@ class NativeEncapsulationApp {
class NativeExampleModule {
}

@Component({
selector: 'app',
template: 'ShadowDom works',
encapsulation: ViewEncapsulation.ShadowDom,
styles: [':host { color: red; }']
})
class ShadowDomEncapsulationApp {
}

@NgModule({
declarations: [ShadowDomEncapsulationApp],
imports: [BrowserModule.withServerTransition({appId: 'test'}), ServerModule],
bootstrap: [ShadowDomEncapsulationApp]
})
class ShadowDomExampleModule {
}

@Component({selector: 'my-child', template: 'Works!'})
class MyChildComponent {
@Input() public attr: boolean;
Expand Down Expand Up @@ -537,6 +554,14 @@ export function main() {
});
}));

it('should handle ViewEncapsulation.ShadowDom', async(() => {
renderModule(ShadowDomExampleModule, {document: doc}).then(output => {
expect(output).not.toBe('');
expect(output).toContain('color: red');
called = true;
});
}));

it('should handle false values on attributes', async(() => {
renderModule(FalseAttributesModule, {document: doc}).then(output => {
expect(output).toBe(
Expand Down
Loading