lit-element: how to efficiently share a property from parent to child custom element
Asked Answered
L

1

9

Question: Is there a way to propagate a property change to a child element without triggering its render function? Currently when I update the property in parent's switchViewModeHandler it triggers a re-render on the child.

Use-case: toggling the parent into 'edit' mode should toggle the same for all of its children as well.

Doubt: Should I use custom events? Problem is that it will be a complex network of nested elements, with events it will become unwieldy to debug pretty quickly (already ran into this issue with Polymer).

Setup: Parent element:

class ParentElement extends LitElement {
  @property() viewMode;

  constructor() {
    this.viewMode = ViewMode.editable;
  }

  static get properties() {
    return {
      viewMode: { type: String, reflect: true },
    };
  }

  private switchViewModeHandler(event: MouseEvent): void {
    this.viewMode =
      (this.viewMode === ViewMode.editing) ? ViewMode.editable : ViewMode.editing; // update my own edit mode
    const options: HTMLElement[] = (Array.from(this.children) as HTMLElement[]);
    options.forEach((item: HTMLElement) => {
      item.viewMode = this.viewMode;
    });
  }

  render() {
    return html`
        <p>parent</p><label class="switch-wrapper">toggle edit mode
          <input type="checkbox"
            ?checked="${this.viewMode === ViewMode.editing}"
            @click="${
              (event: MouseEvent): void => this.switchViewModeHandler(event)
            }"
          />
        </label>
        <slot></slot><!-- child comes from here -->
    `;
  }
}

Child element:

class ChildElement extends LitElement {
  @property() viewMode;

  constructor() {
    super();
    this.viewMode = ViewMode.editable;
  }

  static get properties() {
    return {
      viewMode: { type: String, reflect: true },
    };
  }

  render() {
    console.log('child render() called');
    return html`
      <div class="viewer">child viewer</div>
      <div class="editor mode-${this.viewMode}">child editor</div>
    `;
  }
}

Markup:

<parent-element>
  <child-element data-type="special"></child-element
></parent-element>

Edit mode comes from a simple enum that's imported (omitted here):

export enum ViewMode {
  readOnly = 'readOnly',
  editable = 'editable',
  editing = 'editing',
}

Here's a Codesandbox to play with: https://codesandbox.io/s/v1988qmn75

Luff answered 14/3, 2019 at 21:22 Comment(0)
O
7

Propagating state to light DOM children is a relatively rarely needed pattern, so we don't have it documented well yet, but there are some cases like yours where it's necessary.

The right approach really depends on how controlled the children of the container element are. If you, the author of the parent element, also are the only user of it, and so know exactly what the children can be, then you have some more flexibility.

Options I can think of are:

  • Always set a property on the child, regardless of type. This is like if you had a <child prop=${x}></child> syntax available in lit-html.
  • Fire an event on the children. This is very decoupled, but more cost for little benefit if you don't have children from third parties.
  • Have children register with the parent. More responsibility on the child, but takes advantage of the child's lifecycles to subscribe to state changes.

I'd say your approach to set child state in an event handler isn't far off, except that it's too tied to the user interaction event and not the state itself.

I'd go for something where you always update state in updated():

class ParentElement extends LitElement {
  @property({ type: String, reflect: true })
  viewMode = ViewMode.editable;

  private _onSwitchClicked(event: MouseEvent): void {
    this.viewMode = (this.viewMode === ViewMode.editing)
      ? ViewMode.editable
      : ViewMode.editing;
  }

  private _onSlotChange() {
    this.requestUpdate();
  }

  updated() {
    for (const child of Array.from(this.children)) {
      child.viewMode = this.viewMode;
    }
  }

  render() {
    return html`
      <p>parent</p>
      <label class="switch-wrapper">toggle edit mode
        <input type="checkbox"
            ?checked=${this.viewMode === ViewMode.editing}
            @click=${this._onSwitchClicked}>
      </label>
      <slot @slotchange=${this._onSlotChange}></slot>
    `;
  }
}

If the children are LitElements and the values are primitives, it's ok to always set the properties - they'll be dirty checked in the children.

Note the slotchange event handler on <slot> so we can observe the children.

Oloughlin answered 17/3, 2019 at 23:16 Comment(1)
I also cleaned up a few other things in your example: - Local event handlers using @ don't need to be bound or arrow functions. Method work fine and have slightly lower costs. - You don't need quotes around bindings, unless it's an interpolation. - You can declare property options in the decorator. - You can initialize a field in its declaration, you don't need a constructor.Oloughlin

© 2022 - 2024 — McMap. All rights reserved.