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);
Bind to the property, not the attribute:
<input type="text" .value=${this.data} />
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).