Data binding for Web Components in just a few lines of code

Danny Moerkerke
Aug 19, 2019 · 7 min read

It’s not rocket science and Virtual DOM is usually overkill anyway.

Photo by John Barkiple on Unsplash

Earlier this year I wrote an article in which I claimed that Web Components will eventually replace frontend frameworks.

It generated quite some response, more than I could have hoped for and it gave me some interesting opportunities. Lots of people agreed with me, others were more critical and a few people thought I was completely out of my mind and should be banned from writing code forever. In general, good points were being made on both sides.

The main criticism was that the existing frameworks enable a declarative way of writing views through data binding, which is something native Web Components do not provide out of the box. This in itself is a valid point, but nevertheless data binding is easy to achieve for Web Components as I will demonstrate in this article.

The case for declarative data binding

Data binding was first made popular by frameworks like Angular, Backbone and Ember and is now more or less the de facto way of writing views. It enables the “view as a function of data” which means that whenever some data changes, the associated view will “automatically” update.

No more verbose DOM manipulation to keep data and view in sync, just update the data and the view will follow. A killer feature that no sane developer wants to do without these days. It’s easy to understand why developers choose to use a framework that provides data binding, even for apps for which a framework is clearly overkill. Why go through the hassle of verbose DOM manipulation when a framework provides it out of the box?

But data binding is not magic and you don’t need a whole framework to use it. It is easy, trivial even to implement with Web Components in just a few lines of code.

How it works

Like I said, data binding is not magic. Your view does not “magically” update when the underlying data changes. Somewhere, buried deep down in that framework code are setters that get invoked when the data changes and that take care of updating the view.

AngularJS used the so called “digest cycle”: a dirty-checking mechanism that would continuously check which data had changed so the associated views could be updated.

When React came to the scene it offered a different, allegedly better performing solution called Virtual DOM: a JavaScript representation of the DOM which enables only updating the parts of the DOM that have changed. This works well with lists. Instead of re-rendering the whole list when only a few items have changed, only the changed items are updated.

Image: https://xkcd.com/974/

This is great for very large apps with a complicated UI, but for most apps this is quite frankly overkill. It’s not rocket science to write some code that watches data and updates the associated view when that data changes. The thing is that this data often needs to be passed to child components that also need data binding so you usually end up with a lot of DOM manipulation.

What you need is a way of triggering that same data binding in child components when data is pushed down to them. So whenever the data of the parent component changes and some of that data is bound to the view of a child component, that child component’s view needs to update as well.

One way of doing is this is through a base class for all components and since Web Components are created using JavaScript classes, this is a good fit. By default Web Components extend HTMLElement but we can also create our own base class which extends HTMLElement and extend that:

export class CustomElement extends HTMLElement

And then each Web Component we create extends this CustomElement base class:

export class MyComponent extends CustomElement

If you would like to jump straight into the code you can find it on Github.

The actual data binding is implemented by binding the internal state property of CustomElement to the view:

class CustomElement extends HTMLElement {
constructor() {
super();
this.state = {};
}
}

My first idea was to implement this.state as a Proxy so any mutations to the state object would be automatically intercepted, but since a Proxy can have performance implications I decided to implement a setter which also allows to set multiple properties simultaneously:

class CustomElement extends HTMLElement {
...
setState(newState) {
Object.entries(newState)
.forEach(([key, value]) => {
this.state[key] = this.isObject(this.state[key]) &&
this.isObject(value) ? {...this.state[key], ...value} : value;
});
}
}

The setState method iterates through all entries of the newState object and sets all values to the corresponding properties on this.state, so this is also where we should update the view with the same values.

Binding values to the view is implemented through standard data attributes, in this case data-bind:

<p data-bind="title"></p>

This means that this paragraph’s textContent is bound to the value of this.state.title inside the component which manages the view:

class DemoElement extends CustomElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'}); shadowRoot.innerHTML = `
<p data-bind="title"></p>
`;
}
}
const element = document.querySelector('demo-element');
element.setState({title: 'Hello World'});
// the paragraph will now contain the text "Hello World"

This binding can go to an arbitrary depth so this is also possible:

<p data-bind="user.address.city"></p>element.setState({
user: {
address: {
city: 'Amsterdam'
}
}
});
---> <p>Amsterdam</p>

It’s also possible to bind data to a specific property of a Web Component. In this example the data is bound to the title property of <demo-element>:

<parent-element>
<demo-element data-bind="title:name"></demo-element>
</parent-element>
parentElement.setState({name: 'foo'}); //demoElement.title === 'foo'

The actual updating of the view is implemented inside the updateBindings method of CustomElement. When state is updated through the setState method, it parses the properties which are updated to find the HTML elements that are bound to these properties.

So for example, this:

element.setState({
user: {
address: {
city: 'Amsterdam'
}
}
});

updates this.state.user.address.city inside the Web Component and translates the keys in the data object to user.address.city and uses this to find the element(s) this data is bound to:

const elements = this.shadowRoot.querySelectorAll('[data-bind$="user.address.city"]');

This finds all elements whose data-bind attribute ends with user.address.city (note the $ in data-bind$) so it will find data-bind="user.address.city" but also for example data-bind="name:user.address.city" where the data is specifically bound to the name property.

Whenever data is bound to a specific property of an element, like data-bind="name:user.address.city", the component checks if that element is also a Web Component which extends CustomElement and when it is, it updates that property through its setState method. That way, data binding is propagated all the way down to all child components.

If the element that the data is bound to is a regular HTML element then its textContent will simply be updated. In both cases this provides efficient and surgical updates to the DOM in just a few lines of code.

Where a solution like Virtual DOM really shines is when rendering lists. When for example only a part of the list is changed, Virtual DOM will only update the part that changed instead of rerendering the whole list.

This works by creating DOM nodes when the list is rendered for the first time and then only updating these existing nodes (textContent, attributes etc.) when the list changes. It is cheaper to reuse the already created nodes instead of recreating them by rerendering the whole thing, so for really large list this will be more performant.

You may however wonder if rerendering the whole list will be noticeably slower than something like Virtual DOM when you have an average list of say, 25 items. It might become slow when you try to render 250 items but any sane developer should have implemented pagination by then anyway.

I’m not saying this to put down Virtual DOM since it’s great technology. If you run into a situation where you really need Virtual DOM then by all means use it. I’m just trying to make you think before you reach for a heavyweight solution to a lightweight problem that can be solved in a much simpler way.

The demo for the customElement Github repo contains a <data-repeater> Web Component which renders any array of strings inside li tags that is set to its items property. It rerenders the whole list whenever items is set to a new array but it would also be quite simple to make it reuse the already existing li tags.

Conclusion

So there it is, declarative data binding for Web Components in just a few lines of code. I hope I have clearly demonstrated that data binding is easy to implement and you don’t need to reach for a whole framework to be able to use it.

The code in the Github repo is not a replacement for frameworks like React or Vue.js nor does it intend to be. Frameworks usually provide much more than data binding and this article and the code are to demonstrate that you don’t necessarily need one to achieve declarative data binding.

In addition to data binding, customElement also provides some convenience methods for selecting elements and showing and hiding elements.

I invite you to check out the code and play with it, try to break it, take it apart and give me your feedback which will be greatly appreciated!

The Startup

Medium's largest active publication, followed by +567K people. Follow to join our community.

Danny Moerkerke

Written by

Freelance JavaScript/TypeScript engineer, tech writer and conference speaker, Web Standards enthusiast, craft beer lover, husband, father and foster father.

The Startup

Medium's largest active publication, followed by +567K people. Follow to join our community.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade