feat(core): scoped templates in SlotMixin
This commit is contained in:
parent
42f7026b58
commit
ec03d209c8
4 changed files with 138 additions and 24 deletions
5
.changeset/hip-fishes-flash.md
Normal file
5
.changeset/hip-fishes-flash.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/core': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Support scoped templates in SlotMixin
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
/* eslint-disable class-methods-use-this */
|
/* eslint-disable class-methods-use-this */
|
||||||
import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
||||||
|
import { render } from 'lit';
|
||||||
|
import { isTemplateResult } from 'lit/directive-helpers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../types/SlotMixinTypes').SlotMixin} SlotMixin
|
* @typedef {import('../types/SlotMixinTypes').SlotMixin} SlotMixin
|
||||||
* @typedef {import('../types/SlotMixinTypes').SlotsMap} SlotsMap
|
* @typedef {import('../types/SlotMixinTypes').SlotsMap} SlotsMap
|
||||||
|
* @typedef {import('../index').LitElement} LitElement
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {SlotMixin}
|
* @type {SlotMixin}
|
||||||
* @param {import('@open-wc/dedupe-mixin').Constructor<HTMLElement>} superclass
|
* @param {import('@open-wc/dedupe-mixin').Constructor<LitElement>} superclass
|
||||||
*/
|
*/
|
||||||
const SlotMixinImplementation = superclass =>
|
const SlotMixinImplementation = superclass =>
|
||||||
// eslint-disable-next-line no-unused-vars, no-shadow
|
class SlotMixin extends superclass {
|
||||||
class extends superclass {
|
|
||||||
/**
|
/**
|
||||||
* @return {SlotsMap}
|
* @return {SlotsMap}
|
||||||
*/
|
*/
|
||||||
|
|
@ -27,29 +29,47 @@ const SlotMixinImplementation = superclass =>
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
// @ts-ignore checking this in case we pass LitElement, found no good way to type this...
|
|
||||||
if (super.connectedCallback) {
|
|
||||||
// @ts-ignore checking this in case we pass LitElement, found no good way to type this...
|
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
}
|
|
||||||
this._connectSlotMixin();
|
this._connectSlotMixin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {import('@lion/core').TemplateResult} template
|
||||||
|
*/
|
||||||
|
__renderAsNodes(template) {
|
||||||
|
const tempRenderTarget = document.createElement('div');
|
||||||
|
render(template, tempRenderTarget, this.renderOptions);
|
||||||
|
return Array.from(tempRenderTarget.childNodes);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_connectSlotMixin() {
|
_connectSlotMixin() {
|
||||||
if (!this.__isConnectedSlotMixin) {
|
if (!this.__isConnectedSlotMixin) {
|
||||||
Object.keys(this.slots).forEach(slotName => {
|
Object.keys(this.slots).forEach(slotName => {
|
||||||
if (!this.querySelector(`[slot=${slotName}]`)) {
|
if (!Array.from(this.children).find(el => el.slot === slotName)) {
|
||||||
const slotFactory = this.slots[slotName];
|
const slotContent = this.slots[slotName]();
|
||||||
const slotContent = slotFactory();
|
/** @type {Node[]} */
|
||||||
// ignore non-elements to enable conditional slots
|
let nodes = [];
|
||||||
if (slotContent instanceof Element) {
|
|
||||||
slotContent.setAttribute('slot', slotName);
|
if (isTemplateResult(slotContent)) {
|
||||||
this.appendChild(slotContent);
|
nodes = this.__renderAsNodes(slotContent);
|
||||||
this.__privateSlots.add(slotName);
|
} else if (!Array.isArray(slotContent)) {
|
||||||
|
nodes = [/** @type {Node} */ (slotContent)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nodes.forEach(node => {
|
||||||
|
if (!(node instanceof Node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (node instanceof Element) {
|
||||||
|
node.setAttribute('slot', slotName);
|
||||||
|
}
|
||||||
|
this.appendChild(node);
|
||||||
|
this.__privateSlots.add(slotName);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.__isConnectedSlotMixin = true;
|
this.__isConnectedSlotMixin = true;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
|
import sinon from 'sinon';
|
||||||
import { defineCE, expect, fixture } from '@open-wc/testing';
|
import { defineCE, expect, fixture } from '@open-wc/testing';
|
||||||
import { SlotMixin } from '../src/SlotMixin.js';
|
import { SlotMixin } from '../src/SlotMixin.js';
|
||||||
|
import { LitElement, ScopedElementsMixin, html } from '../index.js';
|
||||||
|
|
||||||
describe('SlotMixin', () => {
|
describe('SlotMixin', () => {
|
||||||
it('inserts provided element into lightdom and sets slot', async () => {
|
it('inserts provided element into lightdom and sets slot', async () => {
|
||||||
const tag = defineCE(
|
const tag = defineCE(
|
||||||
class extends SlotMixin(HTMLElement) {
|
class extends SlotMixin(LitElement) {
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
|
|
@ -19,7 +21,7 @@ describe('SlotMixin', () => {
|
||||||
|
|
||||||
it('does not override user provided slots', async () => {
|
it('does not override user provided slots', async () => {
|
||||||
const tag = defineCE(
|
const tag = defineCE(
|
||||||
class extends SlotMixin(HTMLElement) {
|
class extends SlotMixin(LitElement) {
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
|
|
@ -33,9 +35,28 @@ describe('SlotMixin', () => {
|
||||||
expect(/** @type HTMLParagraphElement */ (el.children[0]).innerText).to.equal('user-content');
|
expect(/** @type HTMLParagraphElement */ (el.children[0]).innerText).to.equal('user-content');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does add when user provided slots are not direct children', async () => {
|
||||||
|
const tag = defineCE(
|
||||||
|
class extends SlotMixin(LitElement) {
|
||||||
|
get slots() {
|
||||||
|
return {
|
||||||
|
...super.slots,
|
||||||
|
content: () => document.createElement('div'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const el = await fixture(`<${tag}><p><span slot="content">user-content</span></p></${tag}>`);
|
||||||
|
const slot = /** @type HTMLDivElement */ (
|
||||||
|
Array.from(el.children).find(elm => elm.slot === 'content')
|
||||||
|
);
|
||||||
|
expect(slot.tagName).to.equal('DIV');
|
||||||
|
expect(slot.innerText).to.equal('');
|
||||||
|
});
|
||||||
|
|
||||||
it('supports complex dom trees as element', async () => {
|
it('supports complex dom trees as element', async () => {
|
||||||
const tag = defineCE(
|
const tag = defineCE(
|
||||||
class extends SlotMixin(HTMLElement) {
|
class extends SlotMixin(LitElement) {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.foo = 'bar';
|
this.foo = 'bar';
|
||||||
|
|
@ -67,7 +88,7 @@ describe('SlotMixin', () => {
|
||||||
it('supports conditional slots', async () => {
|
it('supports conditional slots', async () => {
|
||||||
let renderSlot = true;
|
let renderSlot = true;
|
||||||
const tag = defineCE(
|
const tag = defineCE(
|
||||||
class extends SlotMixin(HTMLElement) {
|
class extends SlotMixin(LitElement) {
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
|
|
@ -92,7 +113,7 @@ describe('SlotMixin', () => {
|
||||||
|
|
||||||
it("allows to check which slots have been created via this._isPrivateSlot('slotname')", async () => {
|
it("allows to check which slots have been created via this._isPrivateSlot('slotname')", async () => {
|
||||||
let renderSlot = true;
|
let renderSlot = true;
|
||||||
class SlotPrivateText extends SlotMixin(HTMLElement) {
|
class SlotPrivateText extends SlotMixin(LitElement) {
|
||||||
get slots() {
|
get slots() {
|
||||||
return {
|
return {
|
||||||
...super.slots,
|
...super.slots,
|
||||||
|
|
@ -116,4 +137,71 @@ describe('SlotMixin', () => {
|
||||||
const elNoSlot = /** @type {SlotPrivateText} */ (await fixture(`<${tag}><${tag}>`));
|
const elNoSlot = /** @type {SlotPrivateText} */ (await fixture(`<${tag}><${tag}>`));
|
||||||
expect(elNoSlot.didCreateConditionalSlot()).to.be.false;
|
expect(elNoSlot.didCreateConditionalSlot()).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('supports templates', async () => {
|
||||||
|
const tag = defineCE(
|
||||||
|
class extends SlotMixin(LitElement) {
|
||||||
|
get slots() {
|
||||||
|
return {
|
||||||
|
...super.slots,
|
||||||
|
template: () => html`<span>text</span>`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`<slot name="template"></slot>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const el = await fixture(`<${tag}><${tag}>`);
|
||||||
|
const slot = /** @type HTMLSpanElement */ (
|
||||||
|
Array.from(el.children).find(elm => elm.slot === 'template')
|
||||||
|
);
|
||||||
|
expect(slot.slot).to.equal('template');
|
||||||
|
expect(slot.tagName).to.equal('SPAN');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports scoped elements', async () => {
|
||||||
|
const scopedSpy = sinon.spy();
|
||||||
|
class ScopedEl extends LitElement {
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
scopedSpy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tag = defineCE(
|
||||||
|
class extends ScopedElementsMixin(SlotMixin(LitElement)) {
|
||||||
|
static get scopedElements() {
|
||||||
|
return {
|
||||||
|
// @ts-expect-error
|
||||||
|
...super.scopedElements,
|
||||||
|
'scoped-el': ScopedEl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get slots() {
|
||||||
|
return {
|
||||||
|
...super.slots,
|
||||||
|
template: () => html`<scoped-el></scoped-el>`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
|
||||||
|
// Not rendered to shadowRoot, notScopedSpy should not be called
|
||||||
|
const notScoped = document.createElement('not-scoped');
|
||||||
|
this.appendChild(notScoped);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`<slot name="template"></slot>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await fixture(`<${tag}><${tag}>`);
|
||||||
|
expect(scopedSpy).to.have.been.called;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
7
packages/core/types/SlotMixinTypes.d.ts
vendored
7
packages/core/types/SlotMixinTypes.d.ts
vendored
|
|
@ -1,6 +1,7 @@
|
||||||
import { Constructor } from '@open-wc/dedupe-mixin';
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
||||||
|
import { TemplateResult, LitElement } from '../index.js';
|
||||||
|
|
||||||
declare function slotFunction(): HTMLElement | undefined;
|
declare function slotFunction(): HTMLElement | Node[] | TemplateResult | undefined;
|
||||||
|
|
||||||
export type SlotsMap = {
|
export type SlotsMap = {
|
||||||
[key: string]: typeof slotFunction;
|
[key: string]: typeof slotFunction;
|
||||||
|
|
@ -48,11 +49,11 @@ export declare class SlotHost {
|
||||||
* };
|
* };
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export declare function SlotMixinImplementation<T extends Constructor<HTMLElement>>(
|
export declare function SlotMixinImplementation<T extends Constructor<LitElement>>(
|
||||||
superclass: T,
|
superclass: T,
|
||||||
): T &
|
): T &
|
||||||
Constructor<SlotHost> &
|
Constructor<SlotHost> &
|
||||||
Pick<typeof SlotHost, keyof typeof SlotHost> &
|
Pick<typeof SlotHost, keyof typeof SlotHost> &
|
||||||
Pick<typeof HTMLElement, keyof typeof HTMLElement>;
|
Pick<typeof LitElement, keyof typeof LitElement>;
|
||||||
|
|
||||||
export type SlotMixin = typeof SlotMixinImplementation;
|
export type SlotMixin = typeof SlotMixinImplementation;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue