5

I'm working with lit-elements via @open-wc and is currently trying to write a nested set of components where the inner component is an input field and some ancestor component has to support some arbitrary rewrite rules like 'numbers are not allowed input'.

What I'm trying to figure out is what the right way to built this is using lit-elements. In React I would use a 'controlled component' see here easily forcing all components to submit to the root component property.

The example below is what I've come up with using Lit-Elements. Is there a better way to do it?

Please note; that the challenge becomes slightly harder since I want to ignore some characters. Without the e.target.value = this.value; at level-5, the input elmement would diverge from the component state on ignored chars. I want the entire chain of components to be correctly in sync, hence the header tags to exemplify.

export class Level1 extends LitElement {
  static get properties() {
    return {
      value: { type: String }
    };
  }

  render() {
    return html`
      <div>
        <h1>${this.value}</h1>
        <level-2 value=${this.value} @input-changed=${this.onInput}></level-2>
      </div>`;
  }

  onInput(e) {
    this.value = e.detail.value.replace(/\d/g, ''); 
  }
}

...

export class Level4 extends LitElement {
  static get properties() {
    return {
      value: { type: String }
    };
  }

  render() {
    return html`
      <div>
        <h4>${this.value}</h4>
        <level-5 value=${this.value}></level-5>
      </div>`;
  }
}

export class Level5 extends LitElement {
  static get properties() {
    return {
      value: { type: String }
    };
  }

  render() {
    return html`
      <div>
        <h5>${this.value}</h5>
        <input .value=${this.value} @input=${this.onInput}></input>
      </div>`;
  }

  onInput(e) {
    let event = new CustomEvent('input-changed', {
      detail: { value: e.target.value },
      bubbles: true,
      composed: true
    });

    e.target.value = this.value;
    this.dispatchEvent(event);
  }
}

export class AppShell extends LitElement {
  constructor() {
    super();
    this.value = 'initial value';
  }

  render() {
    return html`
      <level-1 value=${this.value}></level-1>
    `;
  }
}

Added later

An alternative approach was using the path array in the event to access the input element directly from the root component.

I think it's a worse solution because it results in a stronger coupling accross the components, i.e. by assuming the child component is an input element with a value property.

  onInput(e) {
    const target = e.path[0]; // origin input element
    this.value = e.path[0].value.replace(/\d/g, ''); 
    // controlling the child elements value to adhere to the colletive state
    target.value = this.value;
  }
tugend
  • 133
  • 13
  • Sounds like something you could do by having one mixin used for all yor componenets. That mixin would check if the parent has a validation callback and if so call it before it's own (this way making it a recursive process). – mishu Nov 21 '19 at 06:27

1 Answers1

0

Don't compose your events, handle them in the big parent with your logic there. Have the children send all needed info in the event, try not to rely on target in the parent's event handler.

To receive updates, have your components subscribe in a shared mixin, a la @mishu's suggestion, which uses some state container (here, I present some imaginary state solution)

import { subscribe } from 'some-state-solution';
export const FormMixin = superclass => class extends superclass {
  static get properties() { return { value: { type: String }; } } 
  connectedCallback() {
    super.connectedCallback();
    subscribe(this);
  }
}

Then any component-specific side effects you can handle in updated or the event handler (UI only - do logic in the parent or in the state container)

import { publish } from 'some-state-solution';
class Level1 extends LitElement {
  // ...
  onInput({ detail: { value } }) {
    publish('value', value.replace(/\d/g, '')); 
  } 
}
Benny Powers
  • 5,398
  • 4
  • 32
  • 55
  • That's not as easy in this case, I need to affect original DOM element and the way web components work, you can't just react to a change in an input field in an ancestor and then expect the change to propagate into the DOM again (the components does update though). Then you need to consider force updates which is another issue. – tugend Dec 11 '19 at 18:47
  • If you can use the example above to prove me wrong, I'd redact my statement of course. ^_^ – tugend Dec 11 '19 at 18:50
  • 1
    You can use the `event.composedPath()` method to get the initiating ``. That being said, this ties your form's model to the DOM tightly, which is brittle. You might consider using something like a finite state machine or some other kind of state store, like a Proxy or even a Redux store. Input components would fire events concerning their data only, the parent would catch them and update the model or transition the state, and changes would propagate down via component methods. If I had to couple app to DOM structure like that, I'd rather do it in one place: the parent component. – Benny Powers Dec 13 '19 at 06:18