如何将 属性 值传播到 LitElement 的 child

How to propagate property value down to a child of a LitElement

我想从其他两个小部件组成一个 LitElement 小部件。 基本思想是一个“选择器”小部件选择某些东西,将事件发送到它的 parent “中介”小部件,后者又会更新“查看器”[=69= 中的相关 属性 ] 小部件(事件向上,属性 向下)。 这遵循"mediator pattern" example of the documentation about composition,只是我需要处理完全分离的类。

下面是一个最小的工作示例,它在 LitElement TypeScript starter 模板项目中构建和 运行,所有依赖项 force-updated 和 ncu --upgradeAll

详细说明

期望的行为是当用户在下拉列表中选择某些内容时,查看器小部件确实呈现 item["name"]

到目前为止,我设法将一个事件从选择器发送到调解器,并更新了查看器中的 item_id 属性。但是,这似乎不会触发查看器 item_id 属性 的更新。

使用的方法是在 Mediator (l.36) 中覆盖 updated,循环遍历其 children 并执行 child.setAttribute,这可能非常不优雅。肯定还有另一种更干净的方式,但我未能清楚地理解 update 序列的 Lit.

例子

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

相关问题

您仅在 connectedCallback 中设置项目。 最好通过 getter.

动态获取项目

关于您设置开槽元素属性的问题,我认为除了编程之外别无他法。

<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>