Next

27th Jan 2015

Previous

29th Dec 2014

Simple (yet brittle) two-way data binding

Friday, 9th January 2015

Binding data between your model and view is the big thing at the moment. In this article we will take a look at a very simple implementation using getters and setters on object properties by leveraging Object.defineProperty. I warn you, it will be brittle, and we will only be looking at implementing support for binding data to an elements textContent and using inputs which derive from type="text" to save new states. But it would be a good jumping off point to look into more complex implementations.

We want to be instantiating our model with plain object literals and then be changing values as you normally would on an object literal, by assigning new values to a property. Let's start of by defining our initial model object.

The next step is to build our Model constructor, this is a function that will take our defined object, iterate over it's properties and return an object containing the same values. However, these properties will have getters and setters defined, allowing us to make any required changes to the DOM each time one of these properties is set. First off, let's just copy our properties in to the new object.

Well currently this does nothing, it just copies the data into a new object. Now it's time to start using Object.defineProperty. By defining properties on our object we gain more control because we can provide a descriptor object. This descriptor object can dictate whether the property is:

enumerable: In laymans terms, whether this property will show up in a for..in loop.

configurable: Whether once defined this properties descriptor can be changed.

However, the ones we are interested in are the get and set accessor descriptors. By defining these two functions we gain control of what happens when a property is retrieved or changed.

We are going to need some way in the DOM to denote a Nodes reliance on a piece of data from our model. Let's say that anything with the attribute bind={key} will have the data bound to it's textContent and any input with the attribute model={key} will bind into the data object. We are going to be playing with the DOM so we need to make a utility function to get our elements into an array.

This will now propagate our changes into our DOM Nodes, but what about the other way round. This is where the example becomes a bit more brittle. What we want to do is, on model instatiation bind keyup listeners to all elements that are bound by model to our object.

At this point, let's also set up a bit of html in our page which we can use to test. As you will see, we use our attribute nomenclature here and provide both an input with a model attribute and a static bind for all properties of our original object literal.

Now when you load the page, you should see our data bound to the DOM nodes, and you should also be able to change the values in the input fields which will also update the statically bound nodes aswell. Also, try assigning a new value to your model through the console. Simply try: model.firstName = 'John'. You will see that this kind of assignment will also trigger the DOM nodes to be updated.

As you can probably see, this is a rather contrived example and it doesn't take into account DOM mutations, performance, scope/context, data persistence or different types of input fields.

With a bit of work you could implement a MutationObserver which listens for new nodes in the DOM, checks them against the properties of our model and adds relevant listeners to them. You could also quite easily add support for other types of input fields, having specific handlers and accessors for them.