From 8b7cc43f26811e3fd024b3463f21c2c68fb0f5b5 Mon Sep 17 00:00:00 2001 From: Thijs Louisse Date: Fri, 15 Mar 2024 13:14:01 +0100 Subject: [PATCH] feat: allow SlotRerenderObject to first render on connectedCallback via `firstRenderOnConnected` --- .changeset/funny-numbers-prove.md | 5 ++ docs/fundamentals/systems/core/SlotMixin.md | 7 ++ packages/ui/components/core/src/SlotMixin.js | 5 ++ .../ui/components/core/test/SlotMixin.test.js | 79 ++++++++++++++++++- .../components/core/types/SlotMixinTypes.ts | 9 ++- 5 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 .changeset/funny-numbers-prove.md diff --git a/.changeset/funny-numbers-prove.md b/.changeset/funny-numbers-prove.md new file mode 100644 index 000000000..2ffd74ccb --- /dev/null +++ b/.changeset/funny-numbers-prove.md @@ -0,0 +1,5 @@ +--- +'@lion/ui': patch +--- + +feat: allow SlotRerenderObject to first render on connectedCallback via `firstRenderOnConnected` diff --git a/docs/fundamentals/systems/core/SlotMixin.md b/docs/fundamentals/systems/core/SlotMixin.md index 3a445618f..ad3d3b7e2 100644 --- a/docs/fundamentals/systems/core/SlotMixin.md +++ b/docs/fundamentals/systems/core/SlotMixin.md @@ -104,6 +104,13 @@ A `SlotRerenderObject` looks like this: ```ts { template: TemplateResult; + /** + * For backward compat with traditional light render methods, + * it might be needed to have slot contents available in `connectedCallback`. + * Only enable this for existing components that rely on light content availability in connectedCallback. + * For new components, please align with ReactiveElement/LitElement reactive cycle callbacks. + */ + firstRenderOnConnected?: boolean; } ``` diff --git a/packages/ui/components/core/src/SlotMixin.js b/packages/ui/components/core/src/SlotMixin.js index 5c9bb6bf8..73a5636cb 100644 --- a/packages/ui/components/core/src/SlotMixin.js +++ b/packages/ui/components/core/src/SlotMixin.js @@ -195,6 +195,11 @@ const SlotMixinImplementation = superclass => } else if (isRerenderConfig(slotFunctionResult)) { // Rerenderable slots are scheduled in the "updated loop" this.__slotsThatNeedRerender.add(slotName); + + // For backw. compat, we allow a first render on connectedCallback + if (slotFunctionResult.firstRenderOnConnected) { + this.__rerenderSlot(slotName); + } } else { throw new Error( `Slot "${slotName}" configured inside "get slots()" (in prototype) of ${this.constructor.name} may return these types: TemplateResult | Node | {template:TemplateResult, afterRender?:function} | undefined. diff --git a/packages/ui/components/core/test/SlotMixin.test.js b/packages/ui/components/core/test/SlotMixin.test.js index 884077af5..65682afcf 100644 --- a/packages/ui/components/core/test/SlotMixin.test.js +++ b/packages/ui/components/core/test/SlotMixin.test.js @@ -1,5 +1,5 @@ import sinon from 'sinon'; -import { defineCE, expect, fixture, unsafeStatic, html } from '@open-wc/testing'; +import { defineCE, expect, fixture, fixtureSync, unsafeStatic, html } from '@open-wc/testing'; import { ScopedElementsMixin } from '@open-wc/scoped-elements'; import { SlotMixin } from '@lion/ui/core.js'; import { LitElement } from 'lit'; @@ -300,6 +300,83 @@ describe('SlotMixin', () => { expect(document.activeElement).to.equal(el._focusableNode); expect(el._focusableNode.shadowRoot.activeElement).to.equal(el._focusableNode._buttonNode); }); + + describe('firstRenderOnConnected (for backwards compatibility)', () => { + it('does render on connected when firstRenderOnConnected:true', async () => { + // Start with elem that does not render on connectedCallback + const tag = defineCE( + // @ts-expect-error + class extends SlotMixin(LitElement) { + static properties = { currentValue: Number }; + + constructor() { + super(); + this.currentValue = 0; + } + + get slots() { + return { + ...super.slots, + template: () => ({ + firstRenderOnConnected: true, + template: html`${this.currentValue} `, + }), + }; + } + + render() { + return html``; + } + + get _templateNode() { + return /** @type HTMLSpanElement */ ( + Array.from(this.children).find(elm => elm.slot === 'template') + ); + } + }, + ); + const el = /** @type {* & SlotHost} */ (fixtureSync(`<${tag}>`)); + expect(el._templateNode.slot).to.equal('template'); + expect(el._templateNode.textContent?.trim()).to.equal('0'); + }); + + it('does not render on connected when firstRenderOnConnected:false', async () => { + // Start with elem that does not render on connectedCallback + const tag = defineCE( + // @ts-expect-error + class extends SlotMixin(LitElement) { + static properties = { currentValue: Number }; + + constructor() { + super(); + this.currentValue = 0; + } + + get slots() { + return { + ...super.slots, + template: () => ({ template: html`${this.currentValue} ` }), + }; + } + + render() { + return html``; + } + + get _templateNode() { + return /** @type HTMLSpanElement */ ( + Array.from(this.children).find(elm => elm.slot === 'template') + ); + } + }, + ); + const el = /** @type {* & SlotHost} */ (fixtureSync(`<${tag}>`)); + expect(el._templateNode).to.be.undefined; + await el.updateComplete; + expect(el._templateNode.slot).to.equal('template'); + expect(el._templateNode.textContent?.trim()).to.equal('0'); + }); + }); }); describe('SlotFunctionResult types', () => { diff --git a/packages/ui/components/core/types/SlotMixinTypes.ts b/packages/ui/components/core/types/SlotMixinTypes.ts index 3831d5ee9..fce8bf80c 100644 --- a/packages/ui/components/core/types/SlotMixinTypes.ts +++ b/packages/ui/components/core/types/SlotMixinTypes.ts @@ -8,9 +8,16 @@ export type SlotRerenderObject = { template: TemplateResult; /** * Add logic that will be performed after the render - * @deprecated + * @deprecated use regular ReactiveElement/LitElement reactive cycle callbacks instead */ afterRender?: Function; + /** + * For backward compat with traditional light render methods, + * it might be needed to have slot contents available in `connectedCallback`. + * Only enable this for existing components that rely on light content availability in connectedCallback. + * For new components, please align with ReactiveElement/LitElement reactive cycle callbacks. + */ + firstRenderOnConnected?: boolean; }; export type SlotFunctionResult = TemplateResult | Element | SlotRerenderObject | undefined;