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}>${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}>${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;