From ec03d209c831e82f2fe75784fd62417ae4b6a842 Mon Sep 17 00:00:00 2001 From: Thijs Louisse Date: Fri, 13 Aug 2021 09:55:05 +0200 Subject: [PATCH] feat(core): scoped templates in SlotMixin --- .changeset/hip-fishes-flash.md | 5 ++ packages/core/src/SlotMixin.js | 52 +++++++++---- packages/core/test/SlotMixin.test.js | 98 +++++++++++++++++++++++-- packages/core/types/SlotMixinTypes.d.ts | 7 +- 4 files changed, 138 insertions(+), 24 deletions(-) create mode 100644 .changeset/hip-fishes-flash.md diff --git a/.changeset/hip-fishes-flash.md b/.changeset/hip-fishes-flash.md new file mode 100644 index 000000000..f7303702c --- /dev/null +++ b/.changeset/hip-fishes-flash.md @@ -0,0 +1,5 @@ +--- +'@lion/core': patch +--- + +Support scoped templates in SlotMixin diff --git a/packages/core/src/SlotMixin.js b/packages/core/src/SlotMixin.js index 0b51ee54b..2b21de2b3 100644 --- a/packages/core/src/SlotMixin.js +++ b/packages/core/src/SlotMixin.js @@ -1,18 +1,20 @@ /* eslint-disable class-methods-use-this */ 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').SlotsMap} SlotsMap + * @typedef {import('../index').LitElement} LitElement */ /** * @type {SlotMixin} - * @param {import('@open-wc/dedupe-mixin').Constructor} superclass + * @param {import('@open-wc/dedupe-mixin').Constructor} superclass */ const SlotMixinImplementation = superclass => - // eslint-disable-next-line no-unused-vars, no-shadow - class extends superclass { + class SlotMixin extends superclass { /** * @return {SlotsMap} */ @@ -27,29 +29,47 @@ const SlotMixinImplementation = superclass => } 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(); } + /** + * @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 */ _connectSlotMixin() { if (!this.__isConnectedSlotMixin) { Object.keys(this.slots).forEach(slotName => { - if (!this.querySelector(`[slot=${slotName}]`)) { - const slotFactory = this.slots[slotName]; - const slotContent = slotFactory(); - // ignore non-elements to enable conditional slots - if (slotContent instanceof Element) { - slotContent.setAttribute('slot', slotName); - this.appendChild(slotContent); - this.__privateSlots.add(slotName); + if (!Array.from(this.children).find(el => el.slot === slotName)) { + const slotContent = this.slots[slotName](); + /** @type {Node[]} */ + let nodes = []; + + if (isTemplateResult(slotContent)) { + nodes = this.__renderAsNodes(slotContent); + } 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; diff --git a/packages/core/test/SlotMixin.test.js b/packages/core/test/SlotMixin.test.js index 3a0707142..611c25e57 100644 --- a/packages/core/test/SlotMixin.test.js +++ b/packages/core/test/SlotMixin.test.js @@ -1,10 +1,12 @@ +import sinon from 'sinon'; import { defineCE, expect, fixture } from '@open-wc/testing'; import { SlotMixin } from '../src/SlotMixin.js'; +import { LitElement, ScopedElementsMixin, html } from '../index.js'; describe('SlotMixin', () => { it('inserts provided element into lightdom and sets slot', async () => { const tag = defineCE( - class extends SlotMixin(HTMLElement) { + class extends SlotMixin(LitElement) { get slots() { return { ...super.slots, @@ -19,7 +21,7 @@ describe('SlotMixin', () => { it('does not override user provided slots', async () => { const tag = defineCE( - class extends SlotMixin(HTMLElement) { + class extends SlotMixin(LitElement) { get slots() { return { ...super.slots, @@ -33,9 +35,28 @@ describe('SlotMixin', () => { 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}>

user-content

`); + 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 () => { const tag = defineCE( - class extends SlotMixin(HTMLElement) { + class extends SlotMixin(LitElement) { constructor() { super(); this.foo = 'bar'; @@ -67,7 +88,7 @@ describe('SlotMixin', () => { it('supports conditional slots', async () => { let renderSlot = true; const tag = defineCE( - class extends SlotMixin(HTMLElement) { + class extends SlotMixin(LitElement) { get slots() { return { ...super.slots, @@ -92,7 +113,7 @@ describe('SlotMixin', () => { it("allows to check which slots have been created via this._isPrivateSlot('slotname')", async () => { let renderSlot = true; - class SlotPrivateText extends SlotMixin(HTMLElement) { + class SlotPrivateText extends SlotMixin(LitElement) { get slots() { return { ...super.slots, @@ -116,4 +137,71 @@ describe('SlotMixin', () => { const elNoSlot = /** @type {SlotPrivateText} */ (await fixture(`<${tag}><${tag}>`)); expect(elNoSlot.didCreateConditionalSlot()).to.be.false; }); + + it('supports templates', async () => { + const tag = defineCE( + class extends SlotMixin(LitElement) { + get slots() { + return { + ...super.slots, + template: () => html`text`, + }; + } + + render() { + return html``; + } + }, + ); + 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``, + }; + } + + connectedCallback() { + super.connectedCallback(); + + // Not rendered to shadowRoot, notScopedSpy should not be called + const notScoped = document.createElement('not-scoped'); + this.appendChild(notScoped); + } + + render() { + return html``; + } + }, + ); + + await fixture(`<${tag}><${tag}>`); + expect(scopedSpy).to.have.been.called; + }); }); diff --git a/packages/core/types/SlotMixinTypes.d.ts b/packages/core/types/SlotMixinTypes.d.ts index 6854cdceb..221bc7dc2 100644 --- a/packages/core/types/SlotMixinTypes.d.ts +++ b/packages/core/types/SlotMixinTypes.d.ts @@ -1,6 +1,7 @@ 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 = { [key: string]: typeof slotFunction; @@ -48,11 +49,11 @@ export declare class SlotHost { * }; * } */ -export declare function SlotMixinImplementation>( +export declare function SlotMixinImplementation>( superclass: T, ): T & Constructor & Pick & - Pick; + Pick; export type SlotMixin = typeof SlotMixinImplementation;