From 6a07702b6fd5e2974c7c924286e1ec6c9663c362 Mon Sep 17 00:00:00 2001 From: waldronmatt Date: Sat, 31 Aug 2024 19:28:10 -0400 Subject: [PATCH] feat(lit-override): add custom decorator to get template element by id --- .../src/html/markup-light-dom.html | 4 +- .../src/html/styles-light-dom.html | 4 +- apps/lit-override/src/ts/child-component.ts | 10 +- apps/lit-override/src/ts/lazy-load.ts | 4 +- packages/lit-override/README.md | 17 +- packages/lit-override/custom-elements.json | 167 +++++++++--------- packages/lit-override/custom-elements.md | 77 ++++---- .../components/lit-override-component.spec.ts | 49 +++-- .../src/components/lit-override-component.ts | 10 +- .../adopted-stylesheets-converter.ts | 52 +++--- packages/lit-override/src/decorators/index.ts | 1 + .../src/decorators/query-template-by-id.ts | 60 +++++++ .../template-content-with-fallback.ts | 26 +-- packages/lit-override/src/index.ts | 1 + .../src/mixins/emit-connected-callback.ts | 2 +- packages/lit-override/src/utils/markup.ts | 2 - 16 files changed, 288 insertions(+), 198 deletions(-) create mode 100644 packages/lit-override/src/decorators/index.ts create mode 100644 packages/lit-override/src/decorators/query-template-by-id.ts diff --git a/apps/lit-override/src/html/markup-light-dom.html b/apps/lit-override/src/html/markup-light-dom.html index 94b74bb2..559a8620 100644 --- a/apps/lit-override/src/html/markup-light-dom.html +++ b/apps/lit-override/src/html/markup-light-dom.html @@ -12,11 +12,11 @@

Markup slotted from the light dom and styles applied by the host app!

- +

A child component heading from a template in the light dom!

A child component paragraph from a template in the light dom.

- +

A lit override component heading from a template in the light dom!

A lit override component paragraph from a template in the light dom.

diff --git a/apps/lit-override/src/html/styles-light-dom.html b/apps/lit-override/src/html/styles-light-dom.html index ba1fe8b3..73792155 100644 --- a/apps/lit-override/src/html/styles-light-dom.html +++ b/apps/lit-override/src/html/styles-light-dom.html @@ -25,11 +25,11 @@

Styles slotted from the light dom and markup applied by the host app!

- +

A child component heading from a Lit template in the host app!

A child component paragraph from a Lit template in the host app.

- +

A lit override component heading from a Lit template in the host app!

A lit override component paragraph from a Lit template in the host app.

diff --git a/apps/lit-override/src/ts/child-component.ts b/apps/lit-override/src/ts/child-component.ts index f01bb8c3..2ae342d8 100644 --- a/apps/lit-override/src/ts/child-component.ts +++ b/apps/lit-override/src/ts/child-component.ts @@ -1,8 +1,8 @@ import { LitElement, css, html } from 'lit'; -import { property } from 'lit/decorators.js'; import { EmitConnectedCallback } from '@waldronmatt/lit-override/src/mixins/index.js'; import { templateContentWithFallback } from '@waldronmatt/lit-override/src/directives/index.js'; import { AdoptedStyleSheetsConverter } from '@waldronmatt/lit-override/src/controllers/index.js'; +import { queryTemplateById } from '@waldronmatt/lit-override/src/decorators/index.js'; export class ChildComponent extends EmitConnectedCallback(LitElement) { static styles = css` @@ -17,12 +17,12 @@ export class ChildComponent extends EmitConnectedCallback(LitElement) { } `; - @property({ reflect: true, type: String }) - templateId!: string; + @queryTemplateById() + templateId!: HTMLTemplateElement | null; connectedCallback() { super.connectedCallback(); - new AdoptedStyleSheetsConverter(this, { id: this.templateId }); + new AdoptedStyleSheetsConverter(this, { templateEl: this.templateId }); } markup() { @@ -34,7 +34,7 @@ export class ChildComponent extends EmitConnectedCallback(LitElement) { } protected render() { - return html`${templateContentWithFallback({ fallback: this.markup(), id: this.templateId })}`; + return html`${templateContentWithFallback({ fallback: this.markup(), templateEl: this.templateId })}`; } } diff --git a/apps/lit-override/src/ts/lazy-load.ts b/apps/lit-override/src/ts/lazy-load.ts index bf02e444..9791d2f3 100644 --- a/apps/lit-override/src/ts/lazy-load.ts +++ b/apps/lit-override/src/ts/lazy-load.ts @@ -5,11 +5,11 @@ document.getElementById('load-component')?.addEventListener('click', () => { const container = document.getElementById('component-container'); const component = document.createElement('host-app'); component.innerHTML = ` - +

A heading from a template in the light dom!

A paragraph from a template in the light dom.

- +

A heading from a template in the light dom!

A paragraph from a template in the light dom.

diff --git a/packages/lit-override/README.md b/packages/lit-override/README.md index 64ca4261..40dcd407 100644 --- a/packages/lit-override/README.md +++ b/packages/lit-override/README.md @@ -83,18 +83,27 @@ import { LitElement, html } from 'lit'; import { property } from 'lit/decorators.js'; import { templateContentWithFallback } from '@waldronmatt/lit-override/directives/template-content-with-fallback.js'; import { AdoptedStyleSheetsConverter } from '@waldronmatt/lit-override/controllers/adopted-stylesheets-converter.js'; +import { queryTemplateById } from '@waldronmatt/lit-override/decorators/query-template-by-id.js'; export class ChildComponent extends LitElement { - @property({ reflect: true, type: String }) - templateId!: string; + @queryTemplateById({ fallback: true }) + templateId!: HTMLTemplateElement | null; connectedCallback() { super.connectedCallback(); - new AdoptedStyleSheetsConverter(this, { id: this.templateId }); + new AdoptedStyleSheetsConverter(this, { + clearStyes: true, + templateEl: this.templateId, + }); } protected render() { - return html`${templateContentWithFallback({ fallback: html`

Default markup

`, id: this.templateId })}`; + return html` + ${templateContentWithFallback({ + fallback: html`

Default markup

`, + templateEl: this.templateId, + })} + `; } } ``` diff --git a/packages/lit-override/custom-elements.json b/packages/lit-override/custom-elements.json index 5a4a32eb..48b7cc55 100644 --- a/packages/lit-override/custom-elements.json +++ b/packages/lit-override/custom-elements.json @@ -23,6 +23,14 @@ "package": "./controllers/index.js" } }, + { + "kind": "js", + "name": "*", + "declaration": { + "name": "*", + "package": "./decorators/index.js" + } + }, { "kind": "js", "name": "*", @@ -101,10 +109,8 @@ "kind": "field", "name": "templateId", "type": { - "text": "string" - }, - "attribute": "templateId", - "reflects": true + "text": "HTMLTemplateElement | null" + } }, { "kind": "field", @@ -149,14 +155,19 @@ "name": "connected-callback" } ], - "attributes": [ + "mixins": [ { - "name": "templateId", - "type": { - "text": "string" - }, - "fieldName": "templateId" - }, + "name": "EmitConnectedCallback", + "module": "/src/mixins/emit-connected-callback.js" + } + ], + "superclass": { + "name": "LitElement", + "package": "lit" + }, + "tagName": "lit-override", + "customElement": true, + "attributes": [ { "name": "emitConnectedCallback", "type": { @@ -177,19 +188,7 @@ "module": "src/mixins/emit-connected-callback.ts" } } - ], - "mixins": [ - { - "name": "EmitConnectedCallback", - "module": "/src/mixins/emit-connected-callback.js" - } - ], - "superclass": { - "name": "LitElement", - "package": "lit" - }, - "tagName": "lit-override", - "customElement": true + ] } ], "exports": [ @@ -253,28 +252,27 @@ }, { "kind": "field", - "name": "_template", + "name": "clearStyles", "type": { - "text": "HTMLTemplateElement | null" + "text": "AdoptedStyleSheetsConverterParams['clearStyles']" }, - "privacy": "private", - "default": "null" + "default": "clearStyles" }, { "kind": "field", - "name": "clearStyles", + "name": "templateEl", "type": { - "text": "boolean" + "text": "AdoptedStyleSheetsConverterParams['templateEl']" }, - "default": "clearStyles" + "default": "templateEl" }, { "kind": "field", - "name": "id", + "name": "_shadowRoot", "type": { - "text": "string" + "text": "ShadowRoot" }, - "default": "id" + "default": "(this.host as LitElement).renderRoot" }, { "kind": "method", @@ -286,22 +284,12 @@ }, { "kind": "method", - "name": "getTemplateElement", - "privacy": "private", - "return": { - "type": { - "text": "HTMLTemplateElement | null" - } - } - }, - { - "kind": "method", - "name": "removeComponentStyleTag", + "name": "updateStylesheet", "privacy": "private" }, { "kind": "method", - "name": "handleAdoptedStyleSheets", + "name": "setAdoptedStyleSheets", "privacy": "private", "parameters": [ { @@ -314,7 +302,7 @@ }, { "kind": "method", - "name": "updateStylesheet", + "name": "removeComponentStyleTag", "privacy": "private" } ] @@ -348,7 +336,7 @@ }, { "kind": "javascript-module", - "path": "src/directives/index.ts", + "path": "src/decorators/index.ts", "declarations": [], "exports": [ { @@ -356,48 +344,69 @@ "name": "*", "declaration": { "name": "*", - "package": "./template-content-with-fallback.js" + "package": "./query-template-by-id.js" } } ] }, { "kind": "javascript-module", - "path": "src/directives/template-content-with-fallback.ts", + "path": "src/decorators/query-template-by-id.ts", "declarations": [ { - "kind": "class", - "description": "", - "name": "TemplateContentWithFallbackDirective", - "members": [ + "kind": "function", + "name": "queryTemplateById", + "parameters": [ { - "kind": "field", - "name": "_template", + "name": "{ fallback = false }", + "default": "{}", "type": { - "text": "HTMLTemplateElement | null" - }, - "privacy": "private", - "default": "null" + "text": "{ fallback?: boolean }" + } }, { - "kind": "method", - "name": "getTemplateElement", - "privacy": "private", - "return": { - "type": { - "text": "HTMLTemplateElement | null" - } - }, - "parameters": [ - { - "name": "id", - "type": { - "text": "string" - } - } - ] + "description": "gets a template element if an id is not provided (not cached). Defaults to `false`.", + "name": "fallback" } ], + "description": "queryTemplateById\n\nGets a template element by id that is provided to the `templateId` property.\nWill cache the template element on successful query." + } + ], + "exports": [ + { + "kind": "js", + "name": "queryTemplateById", + "declaration": { + "name": "queryTemplateById", + "module": "src/decorators/query-template-by-id.ts" + } + } + ] + }, + { + "kind": "javascript-module", + "path": "src/directives/index.ts", + "declarations": [], + "exports": [ + { + "kind": "js", + "name": "*", + "declaration": { + "name": "*", + "package": "./template-content-with-fallback.js" + } + } + ] + }, + { + "kind": "javascript-module", + "path": "src/directives/template-content-with-fallback.ts", + "declarations": [ + { + "kind": "class", + "description": "", + "name": "TemplateContentWithFallbackDirective", + "members": [], "superclass": { "name": "Directive", "package": "lit/directive.js" @@ -413,8 +422,8 @@ "name": "fallback" }, { - "description": "unique identifier that points to the id of a `template` element. Defaults to empty string.", - "name": "id" + "description": "a `template` element. Defaults to null.", + "name": "templateEl" } ] } @@ -583,7 +592,7 @@ "type": { "text": "TemplateResult" }, - "description": "TemplateResult\n\n**Note**: Only static markdown is supported." + "description": "TemplateResult" } ], "description": "Applies the given template to the `shadowRoot` of elements." diff --git a/packages/lit-override/custom-elements.md b/packages/lit-override/custom-elements.md index 56e1c224..d0fcd06c 100644 --- a/packages/lit-override/custom-elements.md +++ b/packages/lit-override/custom-elements.md @@ -6,6 +6,7 @@ | ---- | ---- | ----------- | ------ | ---------------------- | | `js` | `*` | \* | | ./components/index.js | | `js` | `*` | \* | | ./controllers/index.js | +| `js` | `*` | \* | | ./decorators/index.js | | `js` | `*` | \* | | ./directives/index.js | | `js` | `*` | \* | | ./mixins/index.js | | `js` | `*` | \* | | ./utils/index.js | @@ -30,12 +31,12 @@ #### Fields -| Name | Privacy | Type | Default | Description | Inherited From | -| ----------------------- | ------- | ---------- | ------- | -------------------------------------------------------------------------------------------- | --------------------- | -| `templateId` | | `string` | | | | -| `emitConnectedCallback` | | `boolean` | `false` | Set prop to use \`connected-callback\` event. Defaults to \`false\`. | EmitConnectedCallback | -| `onConnectedCallback` | | `function` | | A callback function called when connected to the DOM. | EmitConnectedCallback | -| `id` | | `string` | | unique identifier that points to the id of a \`template\` element. Defaults to empty string. | | +| Name | Privacy | Type | Default | Description | Inherited From | +| ----------------------- | ------- | ----------------------------- | ------- | -------------------------------------------------------------------------------------------- | --------------------- | +| `templateId` | | `HTMLTemplateElement \| null` | | | | +| `emitConnectedCallback` | | `boolean` | `false` | Set prop to use \`connected-callback\` event. Defaults to \`false\`. | EmitConnectedCallback | +| `onConnectedCallback` | | `function` | | A callback function called when connected to the DOM. | EmitConnectedCallback | +| `id` | | `string` | | unique identifier that points to the id of a \`template\` element. Defaults to empty string. | | #### Events @@ -47,7 +48,6 @@ | Name | Field | Inherited From | | ----------------------- | --------------------- | --------------------- | -| `templateId` | templateId | | | `emitConnectedCallback` | emitConnectedCallback | EmitConnectedCallback | | `onConnectedCallback` | onConnectedCallback | EmitConnectedCallback | @@ -81,23 +81,22 @@ #### Fields -| Name | Privacy | Type | Default | Description | Inherited From | -| ------------- | ------- | ----------------------------- | ------------- | ----------- | -------------- | -| `host` | | `ReactiveControllerHost` | `host` | | | -| `_template` | private | `HTMLTemplateElement \| null` | `null` | | | -| `clearStyles` | | `boolean` | `clearStyles` | | | -| `id` | | `string` | `id` | | | +| Name | Privacy | Type | Default | Description | Inherited From | +| ------------- | ------- | -------------------------------------------------- | -------------------------------------- | ----------- | -------------- | +| `host` | | `ReactiveControllerHost` | `host` | | | +| `clearStyles` | | `AdoptedStyleSheetsConverterParams['clearStyles']` | `clearStyles` | | | +| `templateEl` | | `AdoptedStyleSheetsConverterParams['templateEl']` | `templateEl` | | | +| `_shadowRoot` | | `ShadowRoot` | `(this.host as LitElement).renderRoot` | | | #### Methods -| Name | Privacy | Description | Parameters | Return | Inherited From | -| -------------------------- | ------- | ----------- | -------------------------------- | ----------------------------- | -------------- | -| `hostConnected` | | | | | | -| `hostUpdated` | | | | | | -| `getTemplateElement` | private | | | `HTMLTemplateElement \| null` | | -| `removeComponentStyleTag` | private | | | | | -| `handleAdoptedStyleSheets` | private | | `styleElement: HTMLStyleElement` | | | -| `updateStylesheet` | private | | | | | +| Name | Privacy | Description | Parameters | Return | Inherited From | +| ------------------------- | ------- | ----------- | -------------------------------- | ------ | -------------- | +| `hostConnected` | | | | | | +| `hostUpdated` | | | | | | +| `updateStylesheet` | private | | | | | +| `setAdoptedStyleSheets` | private | | `styleElement: HTMLStyleElement` | | | +| `removeComponentStyleTag` | private | | | | |
@@ -115,6 +114,30 @@ | ---- | ---- | ----------- | ------ | ---------------------------------- | | `js` | `*` | \* | | ./adopted-stylesheets-converter.js | +## `src/decorators/index.ts`: + +### Exports + +| Kind | Name | Declaration | Module | Package | +| ---- | ---- | ----------- | ------ | ------------------------- | +| `js` | `*` | \* | | ./query-template-by-id.js | + +## `src/decorators/query-template-by-id.ts`: + +### Functions + +| Name | Description | Parameters | Return | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- | ------ | +| `queryTemplateById` | queryTemplateById Gets a template element by id that is provided to the \`templateId\` property. Will cache the template element on successful query. | `{ fallback = false }: { fallback?: boolean }, fallback` | | + +
+ +### Exports + +| Kind | Name | Declaration | Module | Package | +| ---- | ------------------- | ----------------- | -------------------------------------- | ------- | +| `js` | `queryTemplateById` | queryTemplateById | src/decorators/query-template-by-id.ts | | + ## `src/directives/index.ts`: ### Exports @@ -127,18 +150,6 @@ ### class: `TemplateContentWithFallbackDirective` -#### Fields - -| Name | Privacy | Type | Default | Description | Inherited From | -| ----------- | ------- | ----------------------------- | ------- | ----------- | -------------- | -| `_template` | private | `HTMLTemplateElement \| null` | `null` | | | - -#### Methods - -| Name | Privacy | Description | Parameters | Return | Inherited From | -| -------------------- | ------- | ----------- | ------------ | ----------------------------- | -------------- | -| `getTemplateElement` | private | | `id: string` | `HTMLTemplateElement \| null` | | -
### Exports diff --git a/packages/lit-override/src/components/lit-override-component.spec.ts b/packages/lit-override/src/components/lit-override-component.spec.ts index 75e20cbb..ead8f39c 100644 --- a/packages/lit-override/src/components/lit-override-component.spec.ts +++ b/packages/lit-override/src/components/lit-override-component.spec.ts @@ -19,21 +19,14 @@ describe('lit-override', () => { testContainer.remove(); }); - it('renders default slot content when no template is provided', async () => { - const el = await fixture(html`

Default Content

`); - await expect(el).to.be.accessible(); - expect(el.innerHTML).to.contain('

Default Content

'); - expect(el.shadowRoot!.innerHTML).to.contain(''); - }); - it('renders content from the template when provided', async () => { const template = document.createElement('template'); - template.id = 'overrideTemplate'; + template.id = 'markupTemplate'; template.innerHTML = ''; testContainer.appendChild(template); const el = await fixture(html` - +

A paragraph coming from a custom template

`); @@ -43,9 +36,11 @@ describe('lit-override', () => { expect(el.shadowRoot!.innerHTML).to.contain(''); }); - it('handles invalid template IDs gracefully', async () => { - const el = await fixture(html``); - expect(el.shadowRoot?.innerHTML).to.contain(''); + it('renders default slot content when no template is provided', async () => { + const el = await fixture(html`

Default Content

`); + await expect(el).to.be.accessible(); + expect(el.innerHTML).to.contain('

Default Content

'); + expect(el.shadowRoot!.innerHTML).to.contain(''); }); it('applies style tag styles as adoptedStyleSheets', async () => { @@ -54,37 +49,61 @@ describe('lit-override', () => { template.innerHTML = ''; testContainer.appendChild(template); - const el = await fixture(html``); + const el = await fixture(html``); + await expect(el).to.be.accessible(); const adoptedStyles = el.shadowRoot?.adoptedStyleSheets; expect(adoptedStyles?.length).to.equal(1); expect(el.innerHTML).to.not.contain(''); expect(el.shadowRoot?.querySelector('style')).to.not.exist; }); - it('calls onConnectedCallback appropriately', async () => { - const callbackSpy = sinon.spy(); + it('logs an error on invalid template ID', async () => { + const consoleErrorSpy = sinon.spy(console, 'error'); + await fixture(html``); + expect(consoleErrorSpy).to.have.been.calledWith('Template id nonexistentTemplateId could not be found'); + }); + + it('gets a template element as a fallback when templateId is not provided', async () => { + const template = document.createElement('template'); + template.id = 'aTemplate'; + template.innerHTML = ''; + testContainer.appendChild(template); + + const el = await fixture(html``); + + expect(el.shadowRoot!.innerHTML).to.contain(''); + }); + + it('calls onConnectedCallback on connectedCallback', async () => { const el = await fixture(html``); + + const callbackSpy = sinon.spy(); // @ts-expect-error - this is already defined on the component el.onConnectedCallback = callbackSpy; el.connectedCallback(); await aTimeout(50); + expect(callbackSpy).to.have.been.calledOnce; }); it('does not emit connected-callback event when emitConnectedCallback is false', async () => { const el = await fixture(html``); + const eventSpy = sinon.spy(); el.addEventListener('connected-callback', eventSpy); setTimeout(() => el.connectedCallback()); await aTimeout(50); + expect(eventSpy).not.to.have.been.called; }); it('emits connected-callback event when emitConnectedCallback is true', async () => { const el = await fixture(html``); + setTimeout(() => el.connectedCallback()); const ev = await oneEvent(el, 'connected-callback'); + expect(ev).to.exist; }); }); diff --git a/packages/lit-override/src/components/lit-override-component.ts b/packages/lit-override/src/components/lit-override-component.ts index 5cb7f090..783703ee 100644 --- a/packages/lit-override/src/components/lit-override-component.ts +++ b/packages/lit-override/src/components/lit-override-component.ts @@ -2,7 +2,7 @@ import { LitElement, html } from 'lit'; import { EmitConnectedCallback } from '../mixins/emit-connected-callback.js'; import { templateContentWithFallback } from '../directives/template-content-with-fallback.js'; import { AdoptedStyleSheetsConverter } from '../controllers/adopted-stylesheets-converter.js'; -import { property } from 'lit/decorators.js'; +import { queryTemplateById } from '../decorators/query-template-by-id.js'; /** * LitOverride - `` @@ -16,15 +16,15 @@ import { property } from 'lit/decorators.js'; * @slot `` is rendered as fallback if `