Overview

Font-End world, and programming world overall actually, is full of useful frameworks and libraries solving a tremendous amount of issues we face on a daily basis, and this is the reason why they are so popular.

The main benefits of using a framework are that we don't have to redo the work we did on our previous projects and we retain a strong focus on the business logic of the project we are working on. Not to mention the cost reduction for the companies since a developer is a lot cheaper than an architect. The speed of development as well, which is directly linked to the costs...etc. Many other benefits can be accounted for here, but we won't do that since this post has a different perspective.

Not going further from the framework boundaries has a set of consequences. The biggest one is that we stop being developers (we miss out the cool stuff) and become only advanced users of a tool someone created for us. This is the situation we have nowadays and many people think that if they know Angular, Vue or React they are programmers. This is not true, because in that case, all you know is how to set up and configure a tool that works everything for you.

This is all that this post is about. It will cover a topic that many people took for granted since it is so common nowadays, a data binding. I come from an era where data binding wasn't deeply integrated as it is today and there were times when I did everything manually.

Our simple data binding mechanism will have the following classes:

  1. Binding - represents the binding directive
  2. Binder - represents the parser of our directives
  3. TextBindingHandler - represents the text data binding handler
  4. ValueBindingHandler - represents the two-way data binding handler

Ok, let's dive into it.

HTML

Consider the following html:

<!DOCTYPE html>
<html>

<head>
    <title>Vanilla JS Data Binding</title>
</head>

<body>
    <main>
        <input placeholder="Enter name">
        <input placeholder="Enter surname">
        <br>
        <br>
        <span>Entered Person</span>
        <br>
        <span></span>
        <span></span>
    </main>
</body>
</html>

Now, let us say that we want, whenever a name or surname input field changes, to update the information about the entered person below. With Vue, for example, we would just put v-model and v-text directive without worring about it. But doesn't this intrigue you? Don't you wonder how does it work exactly? I surely was intrigued when I first saw the data binding.

Let us update our HTML and use our directives:

<!DOCTYPE html>
<html>

<head>
    <title>Vanilla JS Data Binding</title>
</head>

<body>
    <main>
        <input data-bind="value: name" placeholder="Enter name">
        <input data-bind="value: surname" placeholder="Enter surname">
        <br>
        <br>
        <span>Entered Person</span>
        <br>
        <span data-bind="text: name"></span>
        <span data-bind="text: surname"></span>
    </main>
</body>
</html>

At this point, they won't do anything since we haven't created them yet. Let's start with the Binding class which will be a representation of the data-bind attribute.

JavaScript

In order to make data binding possible, we need to backup it with our language of choice, the JavaScript.

Note that this code is written in next-gen JavaScript and make sure to use one of the latest browsers

Binding Class

The Binding class looks like this:

class Binding {
    constructor(prop, handler, el) {
        this.prop = prop;
        this.handler = handler;
        this.el = el;
    }
    bind() {
        let bindingHandler = Binder.handlers[this.handler];
        bindingHandler.bind(this);
        Binder.subscribe(this.prop, () => {
            bindingHandler.react(this);
        });
    }
    setValue(value) {
        Binder.scope[this.prop] = value;
    }
    getValue() {
        return Binder.scope[this.prop];
    }
}

Our Binding class has three properties and three methods. The prop property will hold the scope, or the viewmodel if you prefer, property name to which we want to bind our element. The handler property will hold the handler key (value or text in our example) we've defined in our Binder class and the el property will hold the HTML element we've bound to.

The method bind does all the magic. It takes the handler based on the provided key and triggers its internal bind method. Also, it subscribes the binding to the scope property and attaches a callback to it for future updates.

Each binding handler must have bind and react method implementations which we will see later in this post.

Methods getValue and setValue retrieve and set the scope value for us respectively.

Binder Class

Let us move to the next class implementation, the Binder class:

class Binder {
    static setScope(scope) {
        this.scope = scope;
    }
    static redefine() {
        let keys = Object.keys(this.scope);
        keys.forEach((key) => {
            let value = this.scope[key];
            delete this.scope[key];
            Object.defineProperty(this.scope, key, {
                get() {
                    return value;
                },
                set(newValue) {
                    const shouldNotify = value != newValue;
                    value = newValue;
                    if (shouldNotify) {
                        Binder.notify(key);
                    };
                }
            })
        });
    }
    static subscribe(key, callback) {
        this.subscriptions.push({
            key: key,
            cb: callback
        });
    }
    static notify(key) {
        const subscriptions = this.subscriptions.filter(
            subscription => subscription.key == key
        );
        subscriptions.forEach(subscription => {
            subscription.cb();
        })
    }
}

// create some static properties
Binder.subscriptions = [];
Binder.scope = {};
Binder.handlers = {
    value: new ValueBindingHandler(),
    text: new TextBindingHandler()
}

This class is going to be used by all our directives, therefore, the methods and properties are defined as static.

We have setScope method. This method is called only once at the application startup. All it does is set up the scope (viewmodel) property of the class. A scope is an object to which we want to bind our view.

Another method called only once is the redefine method. This method has high importance in our program. What it does is that it takes each property of the given viewmodel and redefine it as a reactive one. Without this, it wouldn't be possible to update the UI after our scope updates. The UI update is done via the notify method. This method loops through all subscriptions of a specific scope property and executes the callback attached to it.

Remember the bind method on the Binding class which attaches the callback

In the end, we have a subscribe method which creates a new subscription for the given key/callback pair.

A slightly complex solution would be to store the subscriptions on the actual scope property. This would require to convert them to more complicated observables meaning that our get override wouldn't return only a raw value, but this is a topic for another post.

The Handler Classes

In these classes, we specify what each directive should do initially and after the scope update. As mentioned earlier, we must implement bind and react methods. Let's start with the ValueBindingHandler since it's a two-way binding and it has additional method implementation. The class looks like this:

class ValueBindingHandler {
    bind(binding) {
        binding.el.addEventListener('input', () => {
            this.listener(binding);
        });
        this.react(binding);
    }
    react(binding) {
        binding.el.value = binding.getValue();
    }
    listener(binding) {
        let value = binding.el.value;
        binding.setValue(value);
    }
}

A two-way data binding is exactly what its name says. A binding in two directions. This means that when we update the scope property our bound HTML element must be updated, and vice versa, when we update our HTML element it must update the scope property. This behavior is achieved with an event listener. In our particular case, an input handler is used.

Initially, bind method is called and it's called only once at the application startup. This is done internally, you don't have to call it manually. In this method, we attach an event listener and set the initial value of the scope property to the HTML element (by calling this.react).

Remember the getValue method on the Binding class which returns the scope property value

The listener method is executed whenever we update the input value on our page and it sets the newly entered value to the scope property.

Remember the setValue method on the Binding class which sets the scope property value

In the react method on the other hand, which is called every time when a scope property changes, we set the new value back to the HTML element.

You can update the scope property in the browser console for example. Open it and execute this: Binder.scope.name = 'My Cool Name'

The last class in our example, TextBindingHandler looks like this:

class TextBindingHandler {
    bind(binding) {
        this.react(binding);
    }
    react(binding) {
        binding.el.innerText = binding.getValue();
    }
}

This class is pretty straight-forward. It has two mandatory methods, bind and react which are called upon app initialization and after the scope updates respectively. Since this is a one-way binding on the text property, all we do here is set the innerText of the element.

After setting up your page go ahead and try to update the input fields. The change should update the span text in real time.

Application Startup

In the end, we need to have a code connecting the dots together. An example app initialization looks something like this:

Binder.setScope({
    name: 'John',
    surname: 'Doe'
});
Binder.redefine();

const els = document.querySelectorAll('[data-bind]');
els.forEach(el => {
    const expressionParts = el.getAttribute('data-bind').split(':');
    const bindingHandler = expressionParts[0].trim();
    const scopeKey = expressionParts[1].trim();
    const binding = new Binding(scopeKey, bindingHandler, el);
    binding.bind();
});

Also, don't forget to update the HTML element and include the scripts:

<!DOCTYPE html>
<html>

<head>
    <title>Vanilla JS Data Binding</title>
</head>

<body>
    <main>
        <input data-bind="value: name" placeholder="Enter name">
        <input data-bind="value: surname" placeholder="Enter surname">
        <br>
        <br>
        <span>Entered Person</span>
        <br>
        <span data-bind="text: name"></span>
        <span data-bind="text: surname"></span>
    </main>
</body>

<script src="my-path/TextBindingHandler.js"></script>
<script src="my-path/ValueBindingHandler.js"></script>
<script src="my-path/Binder.js"></script>
<script src="my-path/Binding.js"></script>
<script src="my-path/App.js"></script>

</html>

After this action, everything should work like a charm.

The example above works only on root properties of the given viewmodel. Homework for you would be to update the code and make it possible to work with nested objects and properties. Also, consider implementing unbind mechanism, if you are interested. After a single unbind call, your view should be free again.

Conclusion

If you weren't intrigued before, hopefully, you are now, and I hope I've managed to bring close the mechanism behind the scenes of overwhelmingly popular data binding to you. Stop being shy and ask someone how something was done if you can't figure out by yourself (but do try it though before), and don't forget, there is no such thing as a stupid question, there are only stupid answers.

Thank you for reading and happy coding.