Codementor Events

How and why I built 2-Way Data Binding Library

Published Dec 02, 2017Last updated May 30, 2018
How and why I built 2-Way Data Binding Library

Whenever I think about the benefits of using a framework/library in javascript, I think of the features it saves me time from writing. One of these features is 2-way data binding.

Angular comes into mind when you think of 2-way data binding initially. But of course, there's tons of obfuscated code, you would have to dissect to fully understand the frameworks magic. And if your like me, you want a way to simplify common features, you'll be using to build applications user interfaces. Introducing the concept of writing native javascript features, such as data binding from scratch in order to limit the amount of unnecessary overhead.

2-way data binding is a design concept that means when you change anything in your model, view gets updated and, on changing anything in the view, model gets updated.

Completed Lab: https://codepen.io/ryarborough/project/editor/ZQLEWY

Let's implement 2-way data binding, by writing 4 html input elements in an index.html file.

Name: <input class="name" type="text">
      <input class="name" type="text">
<hr /> 
Age: <input class="age" type="text">
     <input class="age" type="text">

Now for the completed javascript

var $scope = {};
(function () {
    var bindClasses = ["name", "age"];
    var attachEvent = function (classNames) {
        classNames.forEach(function (className) {
            var elements = document.getElementsByClassName(className);
            for (var index in elements) {
                elements[index].onkeyup = function () {
                    for (var index in elements) {
                        elements[index].value = this.value;
                    }
                }
            }
            Object.defineProperty($scope, className, {
                set: function (newValue) {
                    for (var index in elements) {
                        elements[index].value = newValue;
                    }
                }
            });
        });
    };
    attachEvent(bindClasses);
})();
  1. We have taken the classes of the elements on which we need to apply 2-way Data Binding in an array named bindClasses.

  2. Then we have an attachEvent which basically iterates through the classes passed in array bindClasses.

  3. We are extracting all the elements by using their class names document.getElementsByClassName(className).

  4. Once the elements are extracted we are binding onkeyup event on it. When this event is triggered it calls a function which stores the current value inside the element.

  5. We have used object.defineProperty to define a property of an object. Here our object is $scope and property is className.

  6. Then we have a set function which serves as setter of the property.

  7. So, if you do something like - $scope.name="Bobby", "Bobby" would be passed as newValue, which would ultimately replace the value being displayed on the view through the following piece of code elements[index].value = newValue.

Discover and read more posts from Robert Yarborough
get started
post commentsBe the first to share your opinion
Gabiriele Lalasava
6 years ago

Hi Robert, Great writeup. I would suggest a few things:

  1. Make $scope local “private” property. That way it doesn’t interfere globally and prevents mutation.

  2. Provide a DOM selector interface rather than using className. Example:

var elements = document.getElementsByClassName(className);

to

var elements = typeof selector == 'string' ? document.querySelectorAll(selector) : selector;

  1. It’s a cleaner interface if you focus on functionality on individual elements rather than collections.

Instead of a forEach loop in the attachEvent() you should really focus on just the selector passed in.

Then if you want to act on collections, you do:

elements.forEach(attachEvent)

This is a more modular pattern and unless you really need to aggregate for performance or other reasons it’s more “modular” to focus on the smallest unit.

  1. The event handlers should use set using addEventListener rather than mutating onkeyup property.

elements[index].onkeyup = function () {

elements[index].addEventListener('keyup', function () {

  1. There are many more events that need to be handled and not just onkeyup. The event types are also specific to the DOM element they are applied to. Example buttons don’t have an “onkeyup” but can change their value on “onclick” for example.

  2. As Joe mentioned, it’s best to delegate events to a parent that will handle all events. This is more optimized and also can help you handle all the events that mutate the DOM in a more unified way.

  3. DOM mutations are expensive. It’s best to throttle the mutations so the events do not lock the application. Event’s are async but the actual firing of the event is sync. So many DOM operations can slow things down.

elements[index].value = this.value;

to async buffered to next process tick:

setImmediate(() => elements[index].value = this.value);

or async throttled to next process tick and timeout:

var timer, timeout = 100;
clearTimeout(timer);
var timer = setTimeout(() => elements[index].value = this.value, timeout);

I know this is a small sample and nice simplified example of 2 way data binding. Just thought I’d mention those points for a fuller implementation.

Ibrahim Šuta
6 years ago

Thanks Robert and Gabiriele! Interesting post. :)

Joe Tagliaferro
6 years ago

– EDIT –

I wound up writing my own article going into more depth about how I approached creating a two way data binding library
https://www.codementor.io/jtag05/another-two-way-data-binding-article-eimsrneqk

I went a little overboard but here is an approach that would scale.

https://codepen.io/jtag05/project/editor/AkakpM

I removed the dependency on the class selector in favor of a data-scope attribute so that elements can share data without potentially inheriting styles.

I also threw in some extra features like automatically creating a key in the $scope based on the data-scope attribute, handling runtime inserting and removal of DOM elements, and being able to set and syncronize the initial $state data based on an initial value on one of the elements.

Anyway, check it out.

Joe Tagliaferro
6 years ago

First of all, good job on this writeup. If I may suggest a few improvements though.

Utilizing the onKeyUp event misses any right click paste events so I would suggest also subscribing to the onChange event as well.

Also, and more importantly, if this form were to scale upward the increased event handlers would eventually create some slow down.

I haven’t run any benchmarks but I would suggest having a focus event on the body that then creates and the event listeners based on the current target and one of those listeners should be a blur event that destroys the other listeners. That way at any given point you’re only managing one set of event handlers. Then I would use the Observer pattern on $scope as a means of updating the form fields if the data in $scope is updated. I’ll create a codepen and share it here shortly

Show more replies