1

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/lit@2.3.1";

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);
connexo
  • 53,704
  • 14
  • 91
  • 128
exhuma
  • 20,071
  • 12
  • 90
  • 123

1 Answers1

3

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

connexo
  • 53,704
  • 14
  • 91
  • 128
  • 1
    Thank you for the detailed explanation. It makes sense and the attribute/property difference is the piece of the puzzle I was missing and aligns perfectly with my suspicions. I further assume that this is independent from the framework (as it is a "DOM thing) and similar things happen in Vue, React & co. I know you said you are unaware of the reason behind this design-decision. But do you maybe have a link to reference docs on this? I would like to deepen my understanding. If not, that's fine. The most important thing "clicked" for me now :) – exhuma Sep 07 '22 at 04:47
  • 1
    Incidentally, this also helped me understand how to properly use `textarea` element. As those *don't* have a `value` **attribute** I tended to overcomplicate my code. But they *have* a `value` **property** which allows me to use the same technique and remove a *lot* of cruft from my code. – exhuma Sep 07 '22 at 05:00
  • Yeah, for `textarea` the content serves as the initial value. Great to hear you found some missing puzzle pieces! – connexo Sep 07 '22 at 05:20
  • 1
    You never stop learning ;) I'm always excited when I find something I don't understand. It means the knowledge hammer will drop soon and I go away a tiny bit smarter. This issue was one such occasions :) – exhuma Sep 07 '22 at 16:07