From c9844a2f0132c757711a1a1004120ddf058e47f6 Mon Sep 17 00:00:00 2001 From: Rob Wormald Date: Sun, 15 Jul 2018 18:53:18 -0700 Subject: [PATCH] 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 `` element to do native content projection. PR Close #24861 --- browser-providers.conf.js | 2 +- integration/ng_elements/e2e/app.e2e-spec.ts | 2 +- integration/ng_elements/package.json | 3 +- integration/ng_elements/src/app.ts | 28 +++++---------- integration/ng_elements/src/elements.ts | 1 - integration/ng_elements/tsconfig.json | 8 ++--- packages/core/src/render/api.ts | 4 +++ packages/elements/test/slots_spec.ts | 35 +++++-------------- .../testing/src/browser_util.ts | 5 ++- tools/public_api_guard/core/core.d.ts | 2 +- 10 files changed, 32 insertions(+), 58 deletions(-) diff --git a/browser-providers.conf.js b/browser-providers.conf.js index a6f2a2eb10..7676be87eb 100644 --- a/browser-providers.conf.js +++ b/browser-providers.conf.js @@ -46,7 +46,7 @@ var customLaunchers = { 'SL_CHROME': {base: 'SauceLabs', browserName: 'chrome', version: '67'}, 'SL_CHROMEBETA': {base: 'SauceLabs', browserName: 'chrome', version: 'beta'}, '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': {base: 'SauceLabs', platform: 'Windows 10', browserName: 'firefox', version: 'beta'}, 'SL_FIREFOXDEV': diff --git a/integration/ng_elements/e2e/app.e2e-spec.ts b/integration/ng_elements/e2e/app.e2e-spec.ts index 5c5b00c7ce..cae28b48f5 100644 --- a/integration/ng_elements/e2e/app.e2e-spec.ts +++ b/integration/ng_elements/e2e/app.e2e-spec.ts @@ -9,7 +9,7 @@ describe('Element E2E Tests', function () { 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'); const helloWorldEl = element(by.css('hello-world-el')); const input = element(by.css('input[type=text]')); diff --git a/integration/ng_elements/package.json b/integration/ng_elements/package.json index 1553fa7734..5b865031c2 100644 --- a/integration/ng_elements/package.json +++ b/integration/ng_elements/package.json @@ -29,5 +29,6 @@ "serve": "lite-server -c e2e/browser.config.json", "preprotractor": "tsc -p e2e", "protractor": "protractor e2e/protractor.config.js" - } + }, + "private": true } diff --git a/integration/ng_elements/src/app.ts b/integration/ng_elements/src/app.ts index 9ad82f043c..4b5767efcc 100644 --- a/integration/ng_elements/src/app.ts +++ b/integration/ng_elements/src/app.ts @@ -1,8 +1,8 @@ -import {HelloWorldComponent, HelloWorldShadowComponent, TestCardComponent} from './elements'; - -import {NgModule, Injector} from '@angular/core'; -import {BrowserModule} from '@angular/platform-browser'; +import {Injector, NgModule} from '@angular/core'; import {createCustomElement} from '@angular/elements'; +import {BrowserModule} from '@angular/platform-browser'; + +import {HelloWorldComponent, HelloWorldShadowComponent, TestCardComponent} from './elements'; @NgModule({ @@ -11,23 +11,13 @@ import {createCustomElement} from '@angular/elements'; imports: [BrowserModule], }) export class AppModule { - constructor(private injector:Injector){ + constructor(private injector: Injector) { + customElements.define('hello-world-el', createCustomElement(HelloWorldComponent, {injector})); customElements.define( - 'hello-world-el', - createCustomElement(HelloWorldComponent, {injector}) - ); - customElements.define( - 'hello-world-shadow-el', - createCustomElement(HelloWorldShadowComponent, {injector}) - ); - customElements.define( - 'test-card', - createCustomElement(HelloWorldShadowComponent, {injector}) - ); - } - ngDoBootstrap(){ - + 'hello-world-shadow-el', createCustomElement(HelloWorldShadowComponent, {injector})); + customElements.define('test-card', createCustomElement(TestCardComponent, {injector})); } + ngDoBootstrap() {} } export {HelloWorldComponent}; diff --git a/integration/ng_elements/src/elements.ts b/integration/ng_elements/src/elements.ts index 8a41c35e17..95f17182b4 100644 --- a/integration/ng_elements/src/elements.ts +++ b/integration/ng_elements/src/elements.ts @@ -32,5 +32,4 @@ export class HelloWorldShadowComponent { styles: [] }) export class TestCardComponent { - } diff --git a/integration/ng_elements/tsconfig.json b/integration/ng_elements/tsconfig.json index 34cc0b9fb2..1c3d4179be 100644 --- a/integration/ng_elements/tsconfig.json +++ b/integration/ng_elements/tsconfig.json @@ -8,10 +8,8 @@ "compilerOptions": { "module": "es2015", "moduleResolution": "node", - // TODO(i): strictNullChecks should turned on but are temporarily disabled due to #15432 - "strictNullChecks": false, - "target": "es6", - "noImplicitAny": false, + "strictNullChecks": true, + "target": "es2015", "sourceMap": false, "experimentalDecorators": true, "outDir": "built", @@ -27,4 +25,4 @@ "dist", "e2e" ] -} \ No newline at end of file +} diff --git a/packages/core/src/render/api.ts b/packages/core/src/render/api.ts index 1b846f7313..d74996db86 100644 --- a/packages/core/src/render/api.ts +++ b/packages/core/src/render/api.ts @@ -266,6 +266,10 @@ export abstract class Renderer2 { * Implement this callback to prepare an element to be bootstrapped * as a root element, and return the element instance. * @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 `` elements. * @returns The root element. */ abstract selectRootElement(selectorOrNode: string|any, preserveContent?: boolean): any; diff --git a/packages/elements/test/slots_spec.ts b/packages/elements/test/slots_spec.ts index 1271df0ab2..1a8bc7e150 100644 --- a/packages/elements/test/slots_spec.ts +++ b/packages/elements/test/slots_spec.ts @@ -6,19 +6,14 @@ * 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 {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {browserDetection} from '@angular/platform-browser/testing/src/browser_util'; -import {Subject} from 'rxjs'; -import {NgElement, NgElementConstructor, createCustomElement} from '../src/create-custom-element'; -import {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from '../src/element-strategy'; +import {NgElement, createCustomElement} from '../src/create-custom-element'; + -type WithFooBar = { - fooFoo: string, - barBar: string -}; // we only run these tests in browsers that support Shadom DOM slots natively if (browserDetection.supportsCustomElements && browserDetection.supportsShadowDom) { describe('slots', () => { @@ -55,7 +50,7 @@ if (browserDetection.supportsCustomElements && browserDetection.supportsShadowDo const content = testContainer.querySelector('span.projected') !; const slot = testEl.shadowRoot !.querySelector('slot') !; const assignedNodes = slot.assignedNodes(); - expect(assignedNodes[0]).toEqual(content); + expect(assignedNodes[0]).toBe(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 slot = testEl.shadowRoot !.querySelector('slot[name=header]') as HTMLSlotElement; const assignedNodes = slot.assignedNodes(); - expect(assignedNodes[0]).toEqual(content); + expect(assignedNodes[0]).toBe(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 bodySlot = testEl.shadowRoot !.querySelector('slot[name=body]') as HTMLSlotElement; - expect(headerContent.assignedSlot).toEqual(headerSlot); - expect(bodyContent.assignedSlot).toEqual(bodySlot); + expect(headerContent.assignedSlot).toBe(headerSlot); + expect(bodyContent.assignedSlot).toBe(bodySlot); }); it('should listen to slotchange events', (done) => { @@ -94,7 +89,6 @@ if (browserDetection.supportsCustomElements && browserDetection.supportsShadowDo templateEl.innerHTML = tpl; const template = templateEl.content.cloneNode(true) as DocumentFragment; const testEl = template.querySelector('slot-events-el') !as NgElement & SlotEventsComponent; - const content = template.querySelector('span.projected'); testEl.addEventListener('slotEventsChange', e => { expect(testEl.slotEvents.length).toEqual(1); done(); @@ -133,15 +127,6 @@ class NamedSlotsComponent { constructor() {} } -@Component({ - selector: 'default-slots-el', - template: '
', - encapsulation: ViewEncapsulation.ShadowDom -}) -class DefaultSlotsComponent { - constructor() {} -} - @Component({ selector: 'slot-events-el', template: '', @@ -157,10 +142,8 @@ class SlotEventsComponent { } } -const testElements = [ - DefaultSlotComponent, NamedSlotComponent, NamedSlotsComponent, DefaultSlotsComponent, - SlotEventsComponent -]; +const testElements = + [DefaultSlotComponent, NamedSlotComponent, NamedSlotsComponent, SlotEventsComponent]; @NgModule({imports: [BrowserModule], declarations: testElements, entryComponents: testElements}) class TestModule { diff --git a/packages/platform-browser/testing/src/browser_util.ts b/packages/platform-browser/testing/src/browser_util.ts index c7716ebad6..8a3a85daa8 100644 --- a/packages/platform-browser/testing/src/browser_util.ts +++ b/packages/platform-browser/testing/src/browser_util.ts @@ -77,13 +77,12 @@ export class BrowserDetection { get supportsShadowDom() { const testEl = document.createElement('div'); - return (typeof customElements !== 'undefined') && (typeof testEl.attachShadow !== 'undefined'); + return (typeof testEl.attachShadow !== 'undefined'); } get supportsDeprecatedShadowDomV0() { const testEl = document.createElement('div') as any; - return (typeof customElements !== 'undefined') && - (typeof testEl.createShadowRoot !== 'undefined'); + return (typeof testEl.createShadowRoot !== 'undefined'); } } diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 5eecd2a220..7a76067362 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -719,7 +719,7 @@ export declare abstract class Renderer2 { abstract removeChild(parent: any, oldChild: any): void; abstract removeClass(el: any, name: string): 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 setProperty(el: any, name: string, value: any): void; abstract setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2): void;