feat(elements): enable Shadow DOM v1 and slots (#24861)

When using ViewEncapsulation.ShadowDom, Angular will not remove the child nodes of the DOM node a root Component is bootstrapped into. This enables developers building Angular Elements to use the `<slot>` element to do native content projection.

PR Close #24861
This commit is contained in:
Rob Wormald 2018-07-15 18:53:18 -07:00 committed by Misko Hevery
parent 4815b92495
commit c9844a2f01
10 changed files with 32 additions and 58 deletions

View File

@ -46,7 +46,7 @@ var customLaunchers = {
'SL_CHROME': {base: 'SauceLabs', browserName: 'chrome', version: '67'}, 'SL_CHROME': {base: 'SauceLabs', browserName: 'chrome', version: '67'},
'SL_CHROMEBETA': {base: 'SauceLabs', browserName: 'chrome', version: 'beta'}, 'SL_CHROMEBETA': {base: 'SauceLabs', browserName: 'chrome', version: 'beta'},
'SL_CHROMEDEV': {base: 'SauceLabs', browserName: 'chrome', version: 'dev'}, 'SL_CHROMEDEV': {base: 'SauceLabs', browserName: 'chrome', version: 'dev'},
'SL_FIREFOX': {base: 'SauceLabs', browserName: 'firefox', version: '61'}, 'SL_FIREFOX': {base: 'SauceLabs', browserName: 'firefox', version: '60'},
'SL_FIREFOXBETA': 'SL_FIREFOXBETA':
{base: 'SauceLabs', platform: 'Windows 10', browserName: 'firefox', version: 'beta'}, {base: 'SauceLabs', platform: 'Windows 10', browserName: 'firefox', version: 'beta'},
'SL_FIREFOXDEV': 'SL_FIREFOXDEV':

View File

@ -9,7 +9,7 @@ describe('Element E2E Tests', function () {
expect(helloWorldEl.getText()).toEqual('Hello World!'); expect(helloWorldEl.getText()).toEqual('Hello World!');
}); });
it('should display: Hello fromIndex! via name attribute', function () { it('should display: Hello Foo! via name attribute', function () {
browser.get('hello-world.html'); browser.get('hello-world.html');
const helloWorldEl = element(by.css('hello-world-el')); const helloWorldEl = element(by.css('hello-world-el'));
const input = element(by.css('input[type=text]')); const input = element(by.css('input[type=text]'));

View File

@ -29,5 +29,6 @@
"serve": "lite-server -c e2e/browser.config.json", "serve": "lite-server -c e2e/browser.config.json",
"preprotractor": "tsc -p e2e", "preprotractor": "tsc -p e2e",
"protractor": "protractor e2e/protractor.config.js" "protractor": "protractor e2e/protractor.config.js"
} },
"private": true
} }

View File

@ -1,8 +1,8 @@
import {HelloWorldComponent, HelloWorldShadowComponent, TestCardComponent} from './elements'; import {Injector, NgModule} from '@angular/core';
import {NgModule, Injector} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {createCustomElement} from '@angular/elements'; import {createCustomElement} from '@angular/elements';
import {BrowserModule} from '@angular/platform-browser';
import {HelloWorldComponent, HelloWorldShadowComponent, TestCardComponent} from './elements';
@NgModule({ @NgModule({
@ -11,23 +11,13 @@ import {createCustomElement} from '@angular/elements';
imports: [BrowserModule], imports: [BrowserModule],
}) })
export class AppModule { export class AppModule {
constructor(private injector:Injector){ constructor(private injector: Injector) {
customElements.define('hello-world-el', createCustomElement(HelloWorldComponent, {injector}));
customElements.define( customElements.define(
'hello-world-el', 'hello-world-shadow-el', createCustomElement(HelloWorldShadowComponent, {injector}));
createCustomElement(HelloWorldComponent, {injector}) customElements.define('test-card', createCustomElement(TestCardComponent, {injector}));
);
customElements.define(
'hello-world-shadow-el',
createCustomElement(HelloWorldShadowComponent, {injector})
);
customElements.define(
'test-card',
createCustomElement(HelloWorldShadowComponent, {injector})
);
}
ngDoBootstrap(){
} }
ngDoBootstrap() {}
} }
export {HelloWorldComponent}; export {HelloWorldComponent};

View File

@ -32,5 +32,4 @@ export class HelloWorldShadowComponent {
styles: [] styles: []
}) })
export class TestCardComponent { export class TestCardComponent {
} }

View File

@ -8,10 +8,8 @@
"compilerOptions": { "compilerOptions": {
"module": "es2015", "module": "es2015",
"moduleResolution": "node", "moduleResolution": "node",
// TODO(i): strictNullChecks should turned on but are temporarily disabled due to #15432 "strictNullChecks": true,
"strictNullChecks": false, "target": "es2015",
"target": "es6",
"noImplicitAny": false,
"sourceMap": false, "sourceMap": false,
"experimentalDecorators": true, "experimentalDecorators": true,
"outDir": "built", "outDir": "built",
@ -27,4 +25,4 @@
"dist", "dist",
"e2e" "e2e"
] ]
} }

View File

@ -266,6 +266,10 @@ export abstract class Renderer2 {
* Implement this callback to prepare an element to be bootstrapped * Implement this callback to prepare an element to be bootstrapped
* as a root element, and return the element instance. * as a root element, and return the element instance.
* @param selectorOrNode The DOM element. * @param selectorOrNode The DOM element.
* @param preserveContent Whether the contents of the root element
* should be preserved, or cleared upon bootstrap (default behavior).
* Use with `ViewEncapsulation.ShadowDom` to allow simple native
* content projection via `<slot>` elements.
* @returns The root element. * @returns The root element.
*/ */
abstract selectRootElement(selectorOrNode: string|any, preserveContent?: boolean): any; abstract selectRootElement(selectorOrNode: string|any, preserveContent?: boolean): any;

View File

@ -6,19 +6,14 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Component, ComponentFactoryResolver, EventEmitter, Injector, Input, NgModule, Output, ViewEncapsulation, destroyPlatform} from '@angular/core'; import {Component, ComponentFactoryResolver, EventEmitter, Input, NgModule, Output, ViewEncapsulation, destroyPlatform} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util'; import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
import {Subject} from 'rxjs';
import {NgElement, NgElementConstructor, createCustomElement} from '../src/create-custom-element'; import {NgElement, createCustomElement} from '../src/create-custom-element';
import {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from '../src/element-strategy';
type WithFooBar = {
fooFoo: string,
barBar: string
};
// we only run these tests in browsers that support Shadom DOM slots natively // we only run these tests in browsers that support Shadom DOM slots natively
if (browserDetection.supportsCustomElements && browserDetection.supportsShadowDom) { if (browserDetection.supportsCustomElements && browserDetection.supportsShadowDom) {
describe('slots', () => { describe('slots', () => {
@ -55,7 +50,7 @@ if (browserDetection.supportsCustomElements && browserDetection.supportsShadowDo
const content = testContainer.querySelector('span.projected') !; const content = testContainer.querySelector('span.projected') !;
const slot = testEl.shadowRoot !.querySelector('slot') !; const slot = testEl.shadowRoot !.querySelector('slot') !;
const assignedNodes = slot.assignedNodes(); const assignedNodes = slot.assignedNodes();
expect(assignedNodes[0]).toEqual(content); expect(assignedNodes[0]).toBe(content);
}); });
it('should use a named slot to project content', () => { it('should use a named slot to project content', () => {
@ -65,7 +60,7 @@ if (browserDetection.supportsCustomElements && browserDetection.supportsShadowDo
const content = testContainer.querySelector('span.projected') !; const content = testContainer.querySelector('span.projected') !;
const slot = testEl.shadowRoot !.querySelector('slot[name=header]') as HTMLSlotElement; const slot = testEl.shadowRoot !.querySelector('slot[name=header]') as HTMLSlotElement;
const assignedNodes = slot.assignedNodes(); const assignedNodes = slot.assignedNodes();
expect(assignedNodes[0]).toEqual(content); expect(assignedNodes[0]).toBe(content);
}); });
it('should use named slots to project content', () => { it('should use named slots to project content', () => {
@ -81,8 +76,8 @@ if (browserDetection.supportsCustomElements && browserDetection.supportsShadowDo
const headerSlot = testEl.shadowRoot !.querySelector('slot[name=header]') as HTMLSlotElement; const headerSlot = testEl.shadowRoot !.querySelector('slot[name=header]') as HTMLSlotElement;
const bodySlot = testEl.shadowRoot !.querySelector('slot[name=body]') as HTMLSlotElement; const bodySlot = testEl.shadowRoot !.querySelector('slot[name=body]') as HTMLSlotElement;
expect(headerContent.assignedSlot).toEqual(headerSlot); expect(headerContent.assignedSlot).toBe(headerSlot);
expect(bodyContent.assignedSlot).toEqual(bodySlot); expect(bodyContent.assignedSlot).toBe(bodySlot);
}); });
it('should listen to slotchange events', (done) => { it('should listen to slotchange events', (done) => {
@ -94,7 +89,6 @@ if (browserDetection.supportsCustomElements && browserDetection.supportsShadowDo
templateEl.innerHTML = tpl; templateEl.innerHTML = tpl;
const template = templateEl.content.cloneNode(true) as DocumentFragment; const template = templateEl.content.cloneNode(true) as DocumentFragment;
const testEl = template.querySelector('slot-events-el') !as NgElement & SlotEventsComponent; const testEl = template.querySelector('slot-events-el') !as NgElement & SlotEventsComponent;
const content = template.querySelector('span.projected');
testEl.addEventListener('slotEventsChange', e => { testEl.addEventListener('slotEventsChange', e => {
expect(testEl.slotEvents.length).toEqual(1); expect(testEl.slotEvents.length).toEqual(1);
done(); done();
@ -133,15 +127,6 @@ class NamedSlotsComponent {
constructor() {} constructor() {}
} }
@Component({
selector: 'default-slots-el',
template: '<div class="slotparent"><slot name="header"></slot><slot name="body"></slot></div>',
encapsulation: ViewEncapsulation.ShadowDom
})
class DefaultSlotsComponent {
constructor() {}
}
@Component({ @Component({
selector: 'slot-events-el', selector: 'slot-events-el',
template: '<slot (slotchange)="onSlotChange($event)"></slot>', template: '<slot (slotchange)="onSlotChange($event)"></slot>',
@ -157,10 +142,8 @@ class SlotEventsComponent {
} }
} }
const testElements = [ const testElements =
DefaultSlotComponent, NamedSlotComponent, NamedSlotsComponent, DefaultSlotsComponent, [DefaultSlotComponent, NamedSlotComponent, NamedSlotsComponent, SlotEventsComponent];
SlotEventsComponent
];
@NgModule({imports: [BrowserModule], declarations: testElements, entryComponents: testElements}) @NgModule({imports: [BrowserModule], declarations: testElements, entryComponents: testElements})
class TestModule { class TestModule {

View File

@ -77,13 +77,12 @@ export class BrowserDetection {
get supportsShadowDom() { get supportsShadowDom() {
const testEl = document.createElement('div'); const testEl = document.createElement('div');
return (typeof customElements !== 'undefined') && (typeof testEl.attachShadow !== 'undefined'); return (typeof testEl.attachShadow !== 'undefined');
} }
get supportsDeprecatedShadowDomV0() { get supportsDeprecatedShadowDomV0() {
const testEl = document.createElement('div') as any; const testEl = document.createElement('div') as any;
return (typeof customElements !== 'undefined') && return (typeof testEl.createShadowRoot !== 'undefined');
(typeof testEl.createShadowRoot !== 'undefined');
} }
} }

View File

@ -719,7 +719,7 @@ export declare abstract class Renderer2 {
abstract removeChild(parent: any, oldChild: any): void; abstract removeChild(parent: any, oldChild: any): void;
abstract removeClass(el: any, name: string): void; abstract removeClass(el: any, name: string): void;
abstract removeStyle(el: any, style: string, flags?: RendererStyleFlags2): void; abstract removeStyle(el: any, style: string, flags?: RendererStyleFlags2): void;
abstract selectRootElement(selectorOrNode: string | any): any; abstract selectRootElement(selectorOrNode: string | any, preserveContent?: boolean): any;
abstract setAttribute(el: any, name: string, value: string, namespace?: string | null): void; abstract setAttribute(el: any, name: string, value: string, namespace?: string | null): void;
abstract setProperty(el: any, name: string, value: any): void; abstract setProperty(el: any, name: string, value: any): void;
abstract setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2): void; abstract setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2): void;