I want to compose a LitElement widget from two other widgets. The basic idea would be that one "selector" widget selects something, send an event to its parent "mediator" one, which in turns would update the related property in a "viewer" child widget (event up, property down). This follows the "mediator pattern" example of the documentation about composition, only that I need to deal with fully separated classes.
Below is a minimum working example, which builds and run in the LitElement TypeScript starter template project, with all dependencies force-updated with ncu --upgradeAll
.
Detailed description
The desired behavior would be that the Viewer widget does render the item["name"]
when the user selects something in the dropdown list.
So far, I managed to send an event up from the Selector to the Mediator, and to update the item_id
attribute within the Viewer. However, this does not seem to trigger an update of the item_id
property of the Viewer.
The approach used is to override updated
in the Mediator (l.36), loop through its children and do a child.setAttribute
, which is probably highly inelegant. There is surely another —cleaner— way, but I failed to clearly understand the update sequence of Lit.
Example
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Demo</title>
<script src="../node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
<script src="../node_modules/lit/polyfill-support.js"></script>
<script type="module" src="../mediator.js"></script>
<script type="module" src="../selector.js"></script>
<script type="module" src="../viewer.js"></script>
<style>
</style>
</head>
<body>
<h1>Demo</h1>
<my-mediator>
<my-selector slot="selector"></my-selector>
<my-viewer slot="viewer" />
</my-mediator>
</body>
</html>
mediator.ts
import {LitElement, html, css, PropertyValues} from 'lit';
// import {customElement, state} from 'lit/decorators.js';
import {customElement, property} from 'lit/decorators.js';
@customElement('my-mediator')
export class Mediator extends LitElement {
// @state()
@property({type: Number, reflect: true})
item_id: number = Number.NaN;
static override styles = css` `;
override render() {
console.log("[Mediator] rendering");
return html`<h2>Mediator:</h2>
<div @selected=${this.onSelected}>
<slot name="selector" />
</div>
<div>
<slot name="viewer" @slotchange=${this.onSlotChange} />
</div>`;
}
private onSelected(e : CustomEvent) {
console.log("[Mediator] Received selected from selector: ",e.detail.id);
this.item_id = e.detail.id;
this.requestUpdate();
}
private onSlotChange() {
console.log("[Mediator] viewer's slot changed");
this.requestUpdate();
}
override updated(changedProperties:PropertyValues<any>): void {
super.updated(changedProperties);
// It is useless to set the Selector's item_id attribute,
// as it is sent to the Mediator through an event.
for(const child of Array.from(this.children)) {
if(child.slot == "viewer" && !Number.isNaN(this.item_id)) {
console.log("[Mediator] Set child viewer widget's selection to: ", this.item_id);
// FIXME: the item_id attribute is set in the Viewer,
// but it does not trigger an update in the Viewer.
child.setAttribute("item_id", `${this.item_id}`);
}
}
}
}
declare global {
interface HTMLElementTagNameMap {
'my-mediator': Mediator;
}
}
viewer.ts
import {LitElement, html,css} from 'lit';
import {customElement, property} from 'lit/decorators.js';
@customElement('my-viewer')
export class Viewer extends LitElement {
@property({type: Number, reflect: true})
item_id = Number.NaN;
private item: any = {};
static override styles = css` `;
override connectedCallback(): void {
console.log("[Viewer] Callback with item_id",this.item_id);
super.connectedCallback();
if(!Number.isNaN(this.item_id)) {
const items = [
{"name":"item 1","id":1},
{"name":"item 2","id":2},
{"name":"item 3","id":3}
];
this.item = items[this.item_id];
this.requestUpdate();
} else {
console.log("[Viewer] Invalid item_id: ",this.item_id,", I will not go further.");
}
}
override render() {
console.log("[Viewer] rendering");
return html`<h2>Viewer:</h2>
<p>Selected item: ${this.item["name"]}</p>`;
}
}
declare global {
interface HTMLElementTagNameMap {
'my-viewer': Viewer;
}
}
selector.ts
import {LitElement, html,css} from 'lit';
import {customElement, property} from 'lit/decorators.js';
@customElement('my-selector')
export class Selector extends LitElement {
@property({type: Number, reflect: true})
item_id = Number.NaN;
private items: Array<any> = [];
static override styles = css` `;
override connectedCallback(): void {
console.log("[Selector] Callback");
super.connectedCallback();
this.items = [
{"name":"item 0","id":0},
{"name":"item 2","id":2},
{"name":"item 3","id":3}
];
this.item_id = this.items[0].id;
this.requestUpdate();
}
override render() {
console.log("[Selector] Rendering");
// FIXME the "selected" attribute does not appear.
return html`<h2>Selector:</h2>
<select @change=${this.onSelection}>
${this.items.map((item) => html`
<option
value=${item.id}
${this.item_id == item.id ? "selected" : ""}
>${item.name}</option>
`)}
</select>`;
}
private onSelection(e : Event) {
const id: number = Number((e.target as HTMLInputElement).value);
if(!Number.isNaN(id)) {
this.item_id = id;
console.log("[Selector] User selected item: ",this.item_id);
const options = {
detail: {id},
bubbles: true,
composed: true
};
this.dispatchEvent(new CustomEvent('selected',options));
} else {
console.log("[Selector] User selected item, but item_id is",this.item_id);
}
}
}
declare global {
interface HTMLElementTagNameMap {
'my-selector': Selector;
}
}
Related questions
- How to observe property changes with LitElement: old Polymer (Lit's ancestor) version.
- How to send props from parent to child with LitElement: old Polymer version, failed to reproduce.
- How can I get component state change in another component with litElement?: close enough solution to the actually implemented example (does not work as expected).
- What is the correct way to propagate changes from one LitElement to a child LitElement?: old version, does not address two different children.