diff --git a/packages/core/src/render/api.ts b/packages/core/src/render/api.ts
index 22daa2332d..1b846f7313 100644
--- a/packages/core/src/render/api.ts
+++ b/packages/core/src/render/api.ts
@@ -268,7 +268,7 @@ export abstract class Renderer2 {
* @param selectorOrNode The DOM element.
* @returns The root element.
*/
- abstract selectRootElement(selectorOrNode: string|any): any;
+ abstract selectRootElement(selectorOrNode: string|any, preserveContent?: boolean): any;
/**
* Implement this callback to get the parent of a given node
* in the host element's DOM.
diff --git a/packages/core/src/view/element.ts b/packages/core/src/view/element.ts
index 925d71367c..1cc299557c 100644
--- a/packages/core/src/view/element.ts
+++ b/packages/core/src/view/element.ts
@@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
+import {ViewEncapsulation} from '../metadata/view';
import {RendererType2} from '../render/api';
import {SecurityContext} from '../sanitization/security';
@@ -163,7 +164,11 @@ export function createElement(view: ViewData, renderHost: any, def: NodeDef): El
renderer.appendChild(parentEl, el);
}
} else {
- el = renderer.selectRootElement(rootSelectorOrNode);
+ // when using native Shadow DOM, do not clear the root element contents to allow slot projection
+ const preserveContent =
+ (!!elDef.componentRendererType &&
+ elDef.componentRendererType.encapsulation === ViewEncapsulation.ShadowDom);
+ el = renderer.selectRootElement(rootSelectorOrNode, preserveContent);
}
if (elDef.attrs) {
for (let i = 0; i < elDef.attrs.length; i++) {
diff --git a/packages/core/src/view/services.ts b/packages/core/src/view/services.ts
index 32bb76e13e..e25a7b29b2 100644
--- a/packages/core/src/view/services.ts
+++ b/packages/core/src/view/services.ts
@@ -773,9 +773,9 @@ export class DebugRenderer2 implements Renderer2 {
this.delegate.removeChild(parent, oldChild);
}
- selectRootElement(selectorOrNode: string|any): any {
- const el = this.delegate.selectRootElement(selectorOrNode);
- const debugCtx = this.debugContext;
+ selectRootElement(selectorOrNode: string|any, preserveContent?: boolean): any {
+ const el = this.delegate.selectRootElement(selectorOrNode, preserveContent);
+ const debugCtx = getCurrentDebugContext();
if (debugCtx) {
indexDebugNode(new DebugElement(el, null, debugCtx));
}
diff --git a/packages/elements/test/slots_spec.ts b/packages/elements/test/slots_spec.ts
new file mode 100644
index 0000000000..f0e37020ac
--- /dev/null
+++ b/packages/elements/test/slots_spec.ts
@@ -0,0 +1,168 @@
+/**
+ * @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, ComponentFactoryResolver, EventEmitter, Injector, Input, NgModule, Output, ViewEncapsulation, destroyPlatform} from '@angular/core';
+import {BrowserModule} from '@angular/platform-browser';
+import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
+import {Subject} from 'rxjs';
+
+import {NgElement, NgElementConstructor, createCustomElement} from '../src/create-custom-element';
+import {NgElementStrategy, NgElementStrategyEvent, NgElementStrategyFactory} from '../src/element-strategy';
+
+type WithFooBar = {
+ fooFoo: string,
+ barBar: string
+};
+
+if (typeof customElements !== 'undefined') {
+ describe('slots', () => {
+ let testContainer: HTMLDivElement;
+
+ beforeAll(done => {
+ testContainer = document.createElement('div');
+ document.body.appendChild(testContainer);
+ destroyPlatform();
+ platformBrowserDynamic()
+ .bootstrapModule(TestModule)
+ .then(ref => {
+ const injector = ref.injector;
+ const cfr: ComponentFactoryResolver = injector.get(ComponentFactoryResolver);
+
+ testElements.forEach(comp => {
+ const compFactory = cfr.resolveComponentFactory(comp);
+ customElements.define(compFactory.selector, createCustomElement(comp, {injector}));
+ });
+ })
+ .then(done, done.fail);
+ });
+
+ afterAll(() => {
+ destroyPlatform();
+ testContainer.remove();
+ (testContainer as any) = null;
+ });
+
+ it('should use slots to project content', () => {
+ const tpl = ``
+ testContainer.innerHTML = tpl;
+ const testEl = testContainer.querySelector('default-slot-el') !;
+ const content = testContainer.querySelector('span.projected') !;
+ const slot = testEl.shadowRoot !.querySelector('slot') !;
+ const assignedNodes = slot.assignedNodes();
+ expect(assignedNodes[0]).toEqual(content);
+ });
+
+ it('should use a named slot to project content', () => {
+ const tpl = ``
+ testContainer.innerHTML = tpl;
+ const testEl = testContainer.querySelector('named-slot-el') !;
+ 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);
+ });
+
+ it('should use named slots to project content', () => {
+ const tpl = `
+
+
+
+ `
+ testContainer.innerHTML = tpl;
+ const testEl = testContainer.querySelector('named-slots-el') !;
+ const headerContent = testContainer.querySelector('span.projected-header') !;
+ const bodyContent = testContainer.querySelector('span.projected-body') !;
+ 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);
+ });
+
+ it('should listen to slotchange events', (done) => {
+ const templateEl = document.createElement('template');
+ const tpl = `
+
+ Content
+ `
+ 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();
+ });
+ testContainer.appendChild(template);
+ expect(testEl.slotEvents.length).toEqual(0);
+ });
+ });
+}
+
+// Helpers
+@Component({
+ selector: 'default-slot-el',
+ template: '
',
+ encapsulation: ViewEncapsulation.ShadowDom
+})
+class DefaultSlotComponent {
+ constructor() {}
+}
+
+@Component({
+ selector: 'named-slot-el',
+ template: '
',
+ encapsulation: ViewEncapsulation.ShadowDom
+})
+class NamedSlotComponent {
+ constructor() {}
+}
+
+@Component({
+ selector: 'named-slots-el',
+ template: '
',
+ encapsulation: ViewEncapsulation.ShadowDom
+})
+class NamedSlotsComponent {
+ constructor() {}
+}
+
+@Component({
+ selector: 'default-slots-el',
+ template: '
',
+ encapsulation: ViewEncapsulation.ShadowDom
+})
+class DefaultSlotsComponent {
+ constructor() {}
+}
+
+@Component({
+ selector: 'slot-events-el',
+ template: '',
+ encapsulation: ViewEncapsulation.ShadowDom
+})
+class SlotEventsComponent {
+ @Input() slotEvents: Event[] = [];
+ @Output() slotEventsChange = new EventEmitter();
+ constructor() {}
+ onSlotChange(event: Event) {
+ this.slotEvents.push(event);
+ this.slotEventsChange.emit(event);
+ }
+}
+
+const testElements =
+ [
+ DefaultSlotComponent, NamedSlotComponent, NamedSlotsComponent, DefaultSlotsComponent,
+ SlotEventsComponent
+ ]
+
+ @NgModule({imports: [BrowserModule], declarations: testElements, entryComponents: testElements})
+ class TestModule {
+ ngDoBootstrap() {}
+}
diff --git a/packages/platform-browser/animations/src/animation_renderer.ts b/packages/platform-browser/animations/src/animation_renderer.ts
index bdaee22f97..b67eb1a48d 100644
--- a/packages/platform-browser/animations/src/animation_renderer.ts
+++ b/packages/platform-browser/animations/src/animation_renderer.ts
@@ -152,7 +152,9 @@ export class BaseAnimationRenderer implements Renderer2 {
this.engine.onRemove(this.namespaceId, oldChild, this.delegate);
}
- selectRootElement(selectorOrNode: any) { return this.delegate.selectRootElement(selectorOrNode); }
+ selectRootElement(selectorOrNode: any, preserveContent?: boolean) {
+ return this.delegate.selectRootElement(selectorOrNode, preserveContent);
+ }
parentNode(node: any) { return this.delegate.parentNode(node); }
diff --git a/packages/platform-browser/src/dom/dom_renderer.ts b/packages/platform-browser/src/dom/dom_renderer.ts
index 9b583692ce..4c32f03c43 100644
--- a/packages/platform-browser/src/dom/dom_renderer.ts
+++ b/packages/platform-browser/src/dom/dom_renderer.ts
@@ -135,13 +135,15 @@ class DefaultDomRenderer2 implements Renderer2 {
}
}
- selectRootElement(selectorOrNode: string|any): any {
+ selectRootElement(selectorOrNode: string|any, preserveContent?: boolean): any {
let el: any = typeof selectorOrNode === 'string' ? document.querySelector(selectorOrNode) :
selectorOrNode;
if (!el) {
throw new Error(`The selector "${selectorOrNode}" did not match any elements`);
}
- el.textContent = '';
+ if (!preserveContent) {
+ el.textContent = '';
+ }
return el;
}