Skip to content

Commit

Permalink
Merge pull request #2219 from ing-bank/feat/allowFirstSlotRenderOnCon…
Browse files Browse the repository at this point in the history
…nected

feat: allow SlotRerenderObject to to first render on connectedCallbac…
  • Loading branch information
okadurin authored Mar 15, 2024
2 parents a99f2e1 + 8b7cc43 commit d4298f6
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/funny-numbers-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lion/ui': patch
---

feat: allow SlotRerenderObject to first render on connectedCallback via `firstRenderOnConnected`
7 changes: 7 additions & 0 deletions docs/fundamentals/systems/core/SlotMixin.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
```

Expand Down
5 changes: 5 additions & 0 deletions packages/ui/components/core/src/SlotMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
79 changes: 78 additions & 1 deletion packages/ui/components/core/test/SlotMixin.test.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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`<span>${this.currentValue}</span> `,
}),
};
}

render() {
return html`<slot name="template"></slot>`;
}

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`<span>${this.currentValue}</span> ` }),
};
}

render() {
return html`<slot name="template"></slot>`;
}

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', () => {
Expand Down
9 changes: 8 additions & 1 deletion packages/ui/components/core/types/SlotMixinTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit d4298f6

Please sign in to comment.