Understanding two-way data binding in JS

Abstract

In the recent years, two-way data binding has become a very popular technique among all sorts of JavaScript frameworks and other tools, and I felt like an article about the basic concepts behind it might be useful for someone who wishes to understand how exactly two-way data binding works. In this article, I'll try to cover as much of it as possible without going too deep into the mechanics of JavaScript.

What is two-way data binding?

First of all, it is important to point out what exactly two-way data binding is before we can discuss how it works. As an example, let's consider Model-View-Controller (MVC) design pattern. In MVC, so-called Model component represents the Model of the data in the application, while the View component determines how the data is presented to the user. A simple analogy would be to think of the Model as a JavaScript object and the View as an HTML page with some input forms. Take a look at the animation below.

As you can see, the JavaScript object gets updated in real time as the user changes the contents of the input field. Since this is two-way data binding, the opposite is also true: If the application was to update the JS object the changes would appear in the input field straight away.

JavaScript implementation

First of all, let's make a simple HTML page which we'll use to test any JS code we'll write. The code for our page can be found below and it has 3 noteworthy elements:

  1. #input field: The input field we'll use to type in new values for the JS variable. As per the concept of two-way data binding, the changes made here will affect the JS variable and vice versa.
  2. #button button: On click, this button will change the value of the JS variable to Hello. This change should also affect the #input and the #value.
  3. #value field: The value of the JS variable will appear here in plain text. All of the changes made to the variable will reflect in this field in real time.
<!DOCTYPE html>  
<html>  
<head>  
    <title>Two-way Data Binding</title>
</head>  
<body>  
    <input id="input" type="text">
    <button id="button">Set value to "Hello"</button>
    <span>Value: <strong id="value"></strong></span>

    <script>
    /*
     * Our JS will go here
     */
    </script>
</body>  
</html>  

View to Model binding

Let's write some JS! We'll start with the easy part: tracking changes made to the #input field. If you have some experience with web design you probably already know that we're gonna use event listeners. The specific event we'll make use of is the input event. When used on an HTML input tag with type attribute set to text, it's fired when the value of the input is changed. It's significantly different from the keyup event, since keyup is fired whenever any key is released, even if you simply spam the shift key without actually changing the contents of the input field.

The code below does the trick in 3 steps:

  1. It initialises the object that will store the value of our field.
  2. It creates references to the 3 main elements on the page: the input field, the button and the text field that holds the text value of our object.
  3. It sets up an event listener that updates the text value of our object and then logs the new value to the console.
// 1. The object we'll be changing
var data = {  
  variable: ''
};

// 2. References to 3 elements listed above
var inputField = document.getElementById('input');  
var button = document.getElementById('input');  
var valueField = document.getElementById('value');

// 3. Event listener setup
inputField.addEventListener('input', function() {  
    // Update the object
    data.variable = inputField.value;

    // Log the new value to console
    console.log(object.variable);
});

You can see it in action in the demo below. Make sure to open the developer console (usually accessible using F12 key) and type something into the field to confirm that the code works as intended. When you're done, move on to the next section where we'll discuss the implementation of the reverse mechanism.

See the Pen Two-way data binding in JS by Tim K (@TimboKZ) on CodePen.

Model to View binding

This part is a bit trickier than what we've done so far - now we somehow have to "spy" on the object holding our data. Then, whenever said object undergoes any change we have to update the input field with the most recent info.

The watch() method

Turns out Gecko, the engine behind Firefox has a very useful watch() method defined on all JS objects, which does exactly what we want: It tracks all changes made to a property of some object and notifies us about them.

Since this method only exists in Gecko, to use it in browsers you'd have to implement a so-called polyfill for the watch() method, which simply means defining a method that would mirror its functionality. Luckily, this has been done before and this snippet by a fellow named Eli Grey seems to do the trick.

To keep this article short, I will only make use of a small part of the polyfill linked above that is responsible for doing exactly what we're trying to achieve, that is, tracking changes on the properties of the object holding our data. This will make it much easier to understand and even if you want to study the whole thing, understanding this small part first will help you a lot. Keep in mind, the code below is not identical to that in the snippet since I have removed some parts and rewrote the others to make it less ambiguous.

defineProperty() method

As you've probably noticed if you've taken a look at the snippet linked above, its author relied heavily on the defineProperty() method. It is vital to understand why it was used and how it works before we can proceed. I'll briefly talk about that below but I suggest you check the MDN link in this paragraph for a more in-depth description.

As the name implies, definePropery() is a very fancy way of defining a property on a JS object. Consider the example below where we define a method on some object.

var cat1 = {};  
cat1.meow = function() {  
    console.log('Meow!');
}
cat1.meow(); // Output: Meow!

// ... is equivalent to ...

var cat2 = {};  
Object.defineProperty(cat2, 'meow', {  
    configurable: false,
    enumerable: false,
    writable: false,
    value: function() {
        console.log('Meow!');
    }
});
cat2.meow(); // Output: Meow!

As seen in the above example, defineProperty() gives much more control over how you define a property as opposed to doing it using the assignment (=) operator. Another powerful feature it gives us is configuring the getter and setter functions for a property explicitly. It might sound confusing but the example below should make it less ambiguous.

// Simple object property
var person1 = {  
    height: 120
};
person1.height; // Returns 120  
person1.height = 100; // person1.height is now 100


// ... as opposed to ...


// Using defineProperty() `get` and `set`
var person2 = {};  
var height2 = 100;  
Object.defineProperty(person2, 'height', {  
    configurable: false,
    enumerable: false,
    get: function() {
        // Multiply value by 2
        // before returning
        return height2 * 2;
    },
    set: function(value) {
        // Divide the supplied value
        // by 4 before assignment
        height2 = value / 4;
    }
});

person2.height; // Returns 200  
// Think of the line above as person2.height.get()

person2.height = 100; // height2 is now 25  
// Think of the line above as person2.height.set(100)

person2.height; // Returns 50  

It's a lengthy piece of code but it really shows how powerful defineProperty() is. Notice how for person1.height JavaScript engine simply returns and updates the value as we tell it to. At the same time, for person2.height, we have setup all sorts of manipulations that are performed on the value before it is actually returned or is updated. Now consider the fact that the methods get and set can contain any logic we want. Can you see how we can use this to our advantage?

Adding Model to View binding to our page

Now, we're gonna connect all of the small pieces into something greater. Let's start by taking our code from the "View to Model binding" section and make several modifications to it:

  1. Define a function called watch(), That takes 3 arguments: An object, name of the property that exists on said object and a callback, that will be called every time the property changes.
  2. Use the above function on the property variable from the object. The callback we'll use will update our #input and #value with the most recent value of our variable.
  3. Setup the click event on our #button to change the value of the variable to Hello.

Well, shall we start? Read through the code below and locate the changes described above. I added some comments to the new additions.

// Definitions from the first version of the code
var data = {  
  variable: ''
};
var inputField = document.getElementById('input');  
var button = document.getElementById('button');  
var valueField = document.getElementById('value');

// 1. The watch() function described above
var watch = function(object, property, callback) {  
  // Store the initial value for future use
  var value = object[property];

  // Remove the original property since
  // now we want to "spy" on it
  delete object[property];

  // Define the property again,
  // now using `get` and `set`
  Object.defineProperty(object, property, {
    configurable: false,
    enumerable: false,
    get: function() {
      // Simply return the value,
      // nothing special here
      return value;
    },
    set: function(newValue) {
      // Update the value
      value = newValue;
      // Call the callback with
      // the new value
      callback(newValue);
    }
  });
};

// 2. Watch our `data` object for changes using our
// newly defined `watch()` function
watch(data, 'variable', function(newValue) {  
  // Update the input field value
  inputField.value = newValue;

  // Update the text field
  valueField.textContent = newValue;
});

// 3. Setup the `click` event for the button
button.addEventListener('click', function() {  
  // Simply set the value to `Hello`
  data.variable = 'Hello';
});

// Adding View to Model binding. This should come
// after our `watch()` function because `set` would
// not be available before that.
inputField.addEventListener('input', function() {  
  data.variable = inputField.value;
});

The final product

Below you can find the demo of the final product. Although it took quite a while to get here, the end result seems to be working exactly as we expected.

See the Pen Two-way data binding in JS by Tim K (@TimboKZ) on CodePen.

Conclusion

We've gone through quite a lot things in this article and I do realise that the amount of code to comprehend is quite big especially for someone with little prior JavaScript experience, but I hope this article help you understand, even remotely, how two-way data binding works. If you find any mistakes in my code or explanations, please feel free to comment below.

If you found this post useful, feel free to like and share:

Comments