Search code examples
javascripthtmlweb-componentlit

What is the correct way of updating a HTML Input element after data changed in a lit-element?


I have a bug in an application using lit-element custom web-components: When manually editing a HTML Input element, changes in the internal component state are no longer reflected in that element. However, before doing any manual edits, the values are correctly reflected.

I seem to miss some knowledge on how HTML element attributes work. Either that, or I lack knowledge around lit-element.

My suspicion is on the way the DOM node works in detail. But I may be wrong. It seems that as soon as a manual edit it made to the "Input" element, its behaviour changes and it always keeps the user-value, even if something else changes. Conversely though, getting a reference to the input-element in the dev-tools console (via right-click, inspect and then using the $0 variable) allows me to do $0.value = 'foo' and the value 'foo' will be displayed in the field.

So with that last experiment I am not so sure anymore. Has it something to do with lit after all? I'm all out of ideas...

It seems to be a very common thing to do so I would assume that lit should support this, and that I'm doing something wrong somewhere...

I could listen to change events and then set the .value propery manually. But that seems unnecessarily "manual". So how do I do this correctly?

I managed to reduce the issue to a really small reproducible example. I wrote it up on codepen here: https://codepen.io/exhuma/pen/eYrNQJK

For reference, here's the code using and defining the component:

<!-- using the component -->
<text-field-demo></text-field-demo>
<script type="module" src="path-to-ts-file" />
// The TypeScript file to load in the HTML
// document which uses the component

import {
  LitElement,
  html,
  customElement,
  state
} from "https://cdn.skypack.dev/[email protected]";

export class TextFieldDemo extends LitElement {
  // [does not work in CodePen] @state()
  data = 0;

  doIncrement() {
    // This increment should be reflected in the UI
    // even *after* a manual edit was made in
    // the text-field
    this.data += 1;
    console.log(this.data);
    this.requestUpdate();
  }

  /**
   * Update the local component state with the new value from the DOM
   * @param evt The change event
   */
  updateObject(evt) {
    this.data = Number.parseInt(evt.target.value, 10);
  }

  render() {
    return html`
      Object:
      <input type="text" value=${this.data} @change=${this.updateObject} />
      <button @click=${this.doIncrement}>Increment and log</button>
      <pre>Data in non-interactive element: ${this.data}</pre>
    `;
  }
}
customElements.define("text-field-demo", TextFieldDemo);

Solution

  • Short answer

    Bind to the property, not the attribute:

    <input type="text" .value=${this.data} />
    

    Long answer with detailed explanation

    The value attribute of an HTMLInputElement serves only as an initial value (available as defaultValue property on the DOM object) at first render; changing the value attribute later neither has any visual impact, nor does it reflect to the value property (which is always in sync with what the input displays). It does change the defaultValue property, though.

    I don't have exact reason for this design decision for you, but it is helpful if you want to check if an input's content has been modified (el.value !== el.defaultValue).

    See also https://github.com/lit/lit/issues/743#issuecomment-454042498

    So to fix your problem, put a . before the value in your render method (which tells lit to bind to the property rather than to an attribute):

    <input type="text" .value=${this.data} />
    

    If all your change handler does is writing user input back into your state, that's already being done for you.

    Also see https://github.com/lit/lit/issues/743#issuecomment-454057588

    Since this is a very common source of errors, eslint-plugin-lit (which your project apparently isn't using) even has a rule for this (also see this explanation).