Data binding for modern applications is about as fundamental as addition is to mathematics. To write any major application without it would seem absurd. An related to data binding is one of the most fundamental architectural patterns that has birth dozens of progeny, the Model-View-Controller pattern. I wanted to write this post not as an attempt to create a new framework, as there were probably 2 or 3 released in the time it took me to write this. I wanted to write to this show how data binding can be done rather simply with pure JavaScript. As it turns out, it’s not that hard.

Rant (Skip This Part)

Some might call me a JavaScript Fundamentalist that constantly whines about how all those other so-called “JavaScript Programmers” have adopted those frameworks that are supposed to make JavaScript development easier. Regardless though, please don’t here me say, “I hate frameworks”. I don’t. I still use them when necessary and with all their warts frameworks do provide a common platform for developing applications. Developers can at least have a common point of reference which goes a long way when working with a team of developers. With that said, here comes the “but”. I use frameworks, BUT I have two major complaints with frameworks in general.

First is framework fatigue. I’ve been writing JavaScript applications for about as long as JavaScript has existed. Over the course of my 20+ year career now, I’ve seen many frameworks rise and fall. I’ve used a number of them: Backbone, KnockoutJS, AngularJS, Angular 2, Angular what-ever-it-is-now, Ember, Dojo. etc. etc. The nature of these is you learn the framework, and just as you finish your second app, you’re back to square one with the newest, shiniest framework.

My second complaint is plug-in bloat. This stems from the framework ecosystems that these frameworks create. Every time you want to add a widget to an application, you import one or more new JavaScript files into your app on top of everything else that’s already included in the framework. Ultimately, an application includes a bulk of code that never gets executed in the application.

Ironically I’ve come full circles. When I started off writing apps in JavaScript frameworks didn’t exist, so I used pure JavaScript, for what it was, to write apps. Now, if I can, I have prefer to do just this.

Now that I am done ranting, I will step off the soapbox and get to the reason you clicked on this article in the first place: how to do data binding in pure JavaScript.

One-Way Data Binding

A lot of the motivations behind many of the JavaScript frameworks that exists stem for some of the perceived deficiencies in JavaScript, and admittedly there were many. But to date as a language JavaScript has matured to the point where the need for frameworks has been substantially mitigated.

Data binding in concept is quite simple. On one side, you have a data model and on the other side, you have an interface, often called a view. The idea is that you want to “bind” some piece of data to something on the view so that when the data changes, the view changes. This is typical for read-only data. One-way data binding happens only when the model changes and the view is changed.

One-way data binding, while seemingly simpler, is probably the harder one to implement because it requires hooking into JavaScript getters and setters for properties. JavaScript has for a long time had Object.defineProperty. This function allows developers to create custom getters and setters for an object, or even replace them for existing properties. The following code uses this method to change the getter and setter for a previously defined property.

function Binding(b) {
    _this = this
    this.element = b.element    
    this.value = b.object[b.property]
    this.attribute = b.attribute
    this.valueGetter = function(){
        return _this.value;
    }
    this.valueSetter = function(val){
        _this.value = val
        _this.element[_this.attribute] = val
    }

    Object.defineProperty(b.object, b.property, {
        get: this.valueGetter,
        set: this.valueSetter
    }); 
    b.object[b.property] = this.value;

    this.element[this.attribute] = this.value

}

Notice what the code is doing: it’s creating a shadow property to store the value in the Binding object and then using defineProperty to set the getter and setter for the property. Now, whenever the property is set with the equals sign (=), it will call the setter function. The function will set the property and also set the the DOM element to the value. You can see it in action at JSFiddle

Limited Two-Way Data Binding

Two-way data binding happen on with changes are made on the view or model. The same basic code can be used to setup a two-way binding.

Two-Way Data Binding
Two-Way Data Binding

To get DOM feedback, the binding needs an event to listen for. Notice where ‘addEventListener’ is called. This adds an event listener to the element that was passed in. Whenever the event is called, the event handler will set the shadow copy of the object value that is data bound to the element. Changes made to the model will also update the DOM element.

function Binding(b) {
    _this = this
    this.element = b.element
    this.value = b.object[b.property]
    this.attribute = b.attribute
    this.valueGetter = function(){
        return _this.value;
    }
    this.valueSetter = function(val){
        _this.value = val
        _this.element[_this.attribute] = val
    }

    if(b.event){
        this.element.addEventListener(b.event, function(event){
            _this.value = _this.element[_this.attribute]
        })        
    }

    Object.defineProperty(b.object, b.property, {
        get: this.valueGetter,
        set: this.valueSetter
    }); 
    b.object[b.property] = this.value;

    this.element[this.attribute] = this.value
}

You can play with this at JSFiddle.

This code, however, is somewhat limited because it only allows for at most a single one-way or two-way data binding for the element and property.

Better Two-Way Data Binding

A better approach to two-way data binding would be to allow for a property to be bound to one or more elements. This means that the data binding can update multiple elements on the DOM when the value is changed either when a DOM event is fired or the model changes.

Also, notice the addBinding function that was added. This now allows for new elements to be added to the binding with events. These are added to to the elementBindings array. The setter function now iterates over the elementBindings array and updates the property whenever a new value is set.

It can also still be used for one-way data bindings by simply omitting the event parameter when calling addBinding.

function Binding(b) {
    _this = this
    this.elementBindings = []
    this.value = b.object[b.property]
    this.valueGetter = function(){
        return _this.value;
    }
    this.valueSetter = function(val){
        _this.value = val
        for (var i = 0; i < _this.elementBindings.length; i++) {
            var binding=_this.elementBindings[i]
            binding.element[binding.attribute] = val
        }
    }
    this.addBinding = function(element, attribute, event){
        var binding = {
            element: element,
            attribute: attribute
        }
        if (event){
            element.addEventListener(event, function(event){
                _this.valueSetter(element[attribute]);
            })
            binding.event = event
        }       
        this.elementBindings.push(binding)
        element[attribute] = _this.value
        return _this
    }

    Object.defineProperty(b.object, b.property, {
        get: this.valueGetter,
        set: this.valueSetter
    }); 

    b.object[b.property] = this.value;
}

You can play with this one too at JSFiddle.

This implementation is better, but there’s still a lot of work that could be done to it. For instance, there’s no way to do data validation, no templating, no support for arrays, no support for custom UI setters or UI formatters, and a myriad of other things. By the time you add all of this stuff, the word “framework” starts to emerge, which of course there are plenty of those out there. For many of my purposes however, a simple script like this is more than sufficient to do what I need for data binding.