Two way data binding in LitElement

jevisan
4 min readJun 1, 2021

--

It was love at first sight. A lightweight, fast and reliable library based on native technology for building scalable SPA’s. Well, everything comes at a cost I guess.

And the moment I realize LitElement, a very young JavaScript library based on web components, lacked a functionality that is essential, if not crucial, in other frameworks and libraries, I was devastated. Heck! even Polymer (Lit’s ancestor) has two-way binding functionality.

But why tho?

Two-way data binding has been a very recurred functionality in (almost) every JS framework that allows for web component creation. So its absence in this very modern library can be very disheartening. But first lets take a look at this very recurred functionality.

Two-way data binding allows for a component to make changes to its data model and notify these changes to other components. A very common example being a simple input that has its value attribute bound to a property. Frameworks like Polymer can even have an special kind of function, called observers, dedicated to, well… observe that property for changes and trigger events in response.

Polymer example of two-way binding is really simple to implement

This example shows the simplicity in which the property value can be updated the moment the user starts typing in the input. We had also defined an observer function, called _valueChanged so we can see the changes to the property the moment they occur. Without adding anything else, it can even propagate the change event upwards in the DOM, thanks to the notify parameter, so other components (the parent, for example) can react to this change. But why is this functionality abscent in LitElement, being Polymer’s successor, and how can we replicate it?

Well, first of all, let’s begin stating that, as stated in its official docs, LitElement, more than a Framework or a library its a “simple base class for creating fast, lightweight web components that work in any web page with any framework.” So, its understandable that it only offers the most important functionality. That is “to render into shadow DOM, and add[…] API to manage properties and attributes.” So its no surprise a very time and resource consuming functionality like two-way binding has been excluded in LitElement.

Second, all the data binding can be exhausting to manage in a large project with several components. Although useful, abuse two-way data binding can result in bad practices.

Third, in recent years, the front end development world has seen a change towards a one-way flux of data. Libraries like Redux, and its extensive use in all sorts of projects, regardless of size, are a good example of this.

All of this could be considered good justification to leave the two-way binding approach out of the LitElement toolbox. And that allows for a very lightweight and fast component based development option.

So now, how can I implement it in LitElement?

We need to go back to the basics. Let’s take a look at the principles of modules communication.

The best practice in a multi component environment, that follows the low coupling principle is that if you need to communicate two separate entities (or components in this case) you should design the data exchange in a way that’s agnostic to the context. That is, a components shouldn't be concerned about another component implementation. So the information exchange should occur in the following way:

  • The communication from parents to children occurs by calling (or binding) the children methods in the parent

&

  • The communication from children to parent should occur by “signaling”, or emitting events up in the ancestors three.

So…

  • Call (or bind) down
  • Signal up

Easy, right?

Let’s see it in action in LitElement, replicating the functionality used in the Polymer example above.

LitElement two-way binding example

Lets break it down:

  • First, we have our property value which we want to update with the user input
  • Then, the input has bound this property to its value attribute. It’s important to note that at initialization time the property value will be undefined. So we’ll need to define a default value, hence the .value="${this.value || ''}", to establish an empty string as default value.
  • Then, a declarative event binding is established with @input . In this case it will listen to every keystroke by the user.
  • Lastly, we declare an inputHandler function that will actually change the property value, log it to console so we can see its changes and then dispatch an event to the component’s parent to communicate the change.

On a side note, we can always use: event.composedPath()[0].value, event.srcElement.value or this.shadowRoot.querySelector('input').value to access the value instead of e.target.value. Each with their set of features.

Now lets create an instance of our custom text input.

Parent component for our two-way data binding component

In the parent, we only need to import our component class, create a child off this class, the custom-text-input in this example. Then we set its value attribute and define an event listener for the value-changed event, as defined in the child component. Now, to read the actual value set by the user, we define a function valueChanged that receives the event, which can be accessed by e.detail .

As you may think, we can extend this behavior to make as complex data flux as you can imagine. Just be careful no to over saturate with events, otherwise all the simplicity in LitElement’s component definition will be lost to an overly complex data flux.

--

--

jevisan

Frontend developer, Python enthusiast, music lover and videogame geek