LitElement: Two-way data binding

3 Patterns to migrate two-way data binding code from PolymerElement to LitElement

Ronny Roeller
NEXT Engineering
2 min readApr 11, 2019

--

Background

Part of Polymer’s beauty was the simple two-way data binding mechanism. For example, the following snippet would update the name field whenever the value of the input element changed:

<input value="{{name::input}}">

Yet, over the years, we learned that two-way data binding causes lots of headaches in complex applications. Popularized by Redux, unidirectional data flows became the standard. LitElement, the replacement of PolymerElement, therefore doesn’t provide any longer two-way data binding.

Still, applications need to access the values of their child elements. Here are the patterns that we use when migrating from PolymerElement to LitElement.

Pattern 1: Access value on button click

A surprisingly large portion of two-way data binding cases is getting data at a specific point of time, e.g. when the user clicks the [Save] button.

For those situation, we simply request the value from the element when it’s required:

@customElement('my-element')
export class MyElement extends LitElement {
protected render() {
return html`
<input id="name">
<button @click="${this.onSave}">Save</button>
`;
}

private onSave() {
console.log(this.inputEl.value);
}

private get inputEl(): HTMLInputElement {
return this.shadowRoot!.getElementById('name')! as HTMLInputElement;
}
}

Hint: Isolating the getElementById call in a separated method allows proper typing (assuming you use TypeScript) and provides a centralized point to interact with the element, e.g. to call methods of this element.

Pattern 2: Access value on change

Sometimes a value should be saved when the user changed it, e.g. auto save a field when the user removes the focus from the input box.

For this, we trigger the above flow whenever the change event is fired:

@customElement('my-element')
export class MyElement extends LitElement {
protected render() {
return html`
<input id="name" @change="${this.onChange}">
`;
}

private onChange() {
console.log(this.inputEl.value);
}

private get inputEl(): HTMLInputElement {
return this.shadowRoot!.getElementById('name')! as HTMLInputElement;
}
}

Pattern 3: Access value on keystroke

Finally, there are cases when we want to access the value whenever the user types another character, e.g. to enable the save button when a value was entered. For this, the input event is our friend:

@customElement('my-element')
export class MyElement extends LitElement {
@property({type: Boolean})
private canSave: boolean = false;

protected render() {
return html`
<input id="name" @input="${this.onInput}">
<button disabled="${this.canSave}">Save</button>
`;
}

private onInput() {
this.canSave = this.inputEl.value.length > 0;
}

private get inputEl(): HTMLInputElement {
return this.shadowRoot!.getElementById('name')! as HTMLInputElement;
}
}

Happy coding!

--

--

Ronny Roeller
NEXT Engineering

CTO at nextapp.co # Product discovery platform for high performing teams that bring their customers into every decision