Reputation: 1332
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
.
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.
<!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>
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;
}
}
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;
}
}
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;
}
}
Upvotes: 0
Views: 1228
Reputation: 4094
You are setting the item only in the connectedCallback. Better you get the item dynamically via getter.
Regarding your question of setting an attribute of a slotted element, there is in my opinion no other way than programmatically.
<script type="module">
import {
LitElement,
html
} from "https://unpkg.com/lit-element/lit-element.js?module";
class MyMediator extends LitElement {
static get properties() {
return {
item_id: {type: Number},
};
}
onSelected(e) {
this.item_id = e.detail.id;
}
render() {
return html`<h3>Mediator:</h3>
<div @selected=${this.onSelected}>
<slot name="selector" />
</div>
<div>
<slot name="viewer" />
</div>`;
}
updated(changedProperties) {
super.updated(changedProperties);
for(const child of Array.from(this.children)) {
if(child.slot === "viewer" && !Number.isNaN(this.item_id)) {
child.setAttribute("item_id", `${this.item_id}`);
}
}
}
}
class MyViewer extends LitElement {
static get properties() {
return {
item_id: {
type: Number,
}
};
}
constructor() {
super();
this.item = {};
}
render() {
return html`<h3>Viewer:</h3>
<p>Selected item: ${this.getItem()["name"]}</p>`;
}
getItem() {
const items = [
{"name":"item 1","id":1},
{"name":"item 2","id":2},
{"name":"item 3","id":3}
];
return items.find(item => item.id === (this.item_id || 1));
}
}
class MySelector extends LitElement {
static get properties() {
return {
item_id: {type: Number},
};
}
constructor() {
super();
this.items = [];
}
connectedCallback() {
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;
}
render() {
return html`<h3>Selector:</h3>
<select @change=${this.onSelection}>
${this.items.map((item) => html`
<option
value=${item.id}
${this.item_id === item.id ? "selected" : ""}
>${item.name}</option>
`)}
</select>`;
}
onSelection(e) {
const id = Number(e.target.value);
if(!Number.isNaN(id)) {
this.item_id = id;
const options = {
detail: {id},
bubbles: true,
composed: true
};
this.dispatchEvent(new CustomEvent('selected',options));
}
}
}
customElements.define("my-mediator", MyMediator);
customElements.define("my-viewer", MyViewer);
customElements.define("my-selector", MySelector);
</script>
<my-mediator>
<my-selector slot="selector"></my-selector>
<my-viewer slot="viewer" />
</my-mediator>
Upvotes: 1