fix(core): do not clear element content when using shadow dom (#24861)
PR Close #24861
This commit is contained in:
parent
1f59f2f04d
commit
6e828bba88
|
@ -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.
|
||||
|
|
|
@ -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++) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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 = `<default-slot-el><span class="projected"></span></default-slot-el>`
|
||||
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 = `<named-slot-el><span class="projected" slot="header"></span></named-slot-el>`
|
||||
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 = `
|
||||
<named-slots-el>
|
||||
<span class="projected-header" slot="header"></span>
|
||||
<span class="projected-body" slot="body"></span>
|
||||
</named-slots-el>`
|
||||
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 = `
|
||||
<slot-events-el>
|
||||
<span class="projected">Content</span>
|
||||
</slot-events-el>`
|
||||
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: '<div class="slotparent"><slot></slot></div>',
|
||||
encapsulation: ViewEncapsulation.ShadowDom
|
||||
})
|
||||
class DefaultSlotComponent {
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'named-slot-el',
|
||||
template: '<div class="slotparent"><slot name="header"></slot></div>',
|
||||
encapsulation: ViewEncapsulation.ShadowDom
|
||||
})
|
||||
class NamedSlotComponent {
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'named-slots-el',
|
||||
template: '<div class="slotparent"><slot name="header"></slot><slot name="body"></slot></div>',
|
||||
encapsulation: ViewEncapsulation.ShadowDom
|
||||
})
|
||||
class NamedSlotsComponent {
|
||||
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({
|
||||
selector: 'slot-events-el',
|
||||
template: '<slot (slotchange)="onSlotChange($event)"></slot>',
|
||||
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() {}
|
||||
}
|
|
@ -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); }
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue