Merge pull request #2219 from ing-bank/feat/allowFirstSlotRenderOnConnected

feat: allow SlotRerenderObject to to first render on connectedCallbac…
This commit is contained in:
Oleksii Kadurin 2024-03-15 15:08:18 +01:00 committed by GitHub
commit d4298f69e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 103 additions and 2 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/ui': patch
---
feat: allow SlotRerenderObject to first render on connectedCallback via `firstRenderOnConnected`

View file

@ -104,6 +104,13 @@ A `SlotRerenderObject` looks like this:
```ts ```ts
{ {
template: TemplateResult; 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;
} }
``` ```

View file

@ -195,6 +195,11 @@ const SlotMixinImplementation = superclass =>
} else if (isRerenderConfig(slotFunctionResult)) { } else if (isRerenderConfig(slotFunctionResult)) {
// Rerenderable slots are scheduled in the "updated loop" // Rerenderable slots are scheduled in the "updated loop"
this.__slotsThatNeedRerender.add(slotName); this.__slotsThatNeedRerender.add(slotName);
// For backw. compat, we allow a first render on connectedCallback
if (slotFunctionResult.firstRenderOnConnected) {
this.__rerenderSlot(slotName);
}
} else { } else {
throw new Error( 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. `Slot "${slotName}" configured inside "get slots()" (in prototype) of ${this.constructor.name} may return these types: TemplateResult | Node | {template:TemplateResult, afterRender?:function} | undefined.

View file

@ -1,5 +1,5 @@
import sinon from 'sinon'; 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 { ScopedElementsMixin } from '@open-wc/scoped-elements';
import { SlotMixin } from '@lion/ui/core.js'; import { SlotMixin } from '@lion/ui/core.js';
import { LitElement } from 'lit'; import { LitElement } from 'lit';
@ -300,6 +300,83 @@ describe('SlotMixin', () => {
expect(document.activeElement).to.equal(el._focusableNode); expect(document.activeElement).to.equal(el._focusableNode);
expect(el._focusableNode.shadowRoot.activeElement).to.equal(el._focusableNode._buttonNode); 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', () => { describe('SlotFunctionResult types', () => {

View file

@ -8,9 +8,16 @@ export type SlotRerenderObject = {
template: TemplateResult; template: TemplateResult;
/** /**
* Add logic that will be performed after the render * Add logic that will be performed after the render
* @deprecated * @deprecated use regular ReactiveElement/LitElement reactive cycle callbacks instead
*/ */
afterRender?: Function; 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; export type SlotFunctionResult = TemplateResult | Element | SlotRerenderObject | undefined;