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:
parent
4815b92495
commit
c9844a2f01
|
@ -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':
|
||||||
|
|
|
@ -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]'));
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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};
|
||||||
|
|
|
@ -32,5 +32,4 @@ export class HelloWorldShadowComponent {
|
||||||
styles: []
|
styles: []
|
||||||
})
|
})
|
||||||
export class TestCardComponent {
|
export class TestCardComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue