Ever wondered 🤔 how data binding works in modern UI frameworks & libraries ?
Tags: javascript, react, angular, opensource
Published: 2022-11-21 03:34:21

We can implement our own data binder which will sync view with model and vice versa. With the release of [ES6 Proxy object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), it's now more simpler to implement our own data binding functionality.

![2-way data binding demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vitjs2qiawtt9j9xzuw2.gif)

Before implementing our own, let's have a brief introduction of Proxy object.

Proxy object gives us the functionality to wrap an original object & then we can access the properties of original object through that Proxy object.

The advantage of using Proxy object is that we can easily attach a handler to the Proxy object so that we can intercept in between if someone tries to access that Proxy object because control will go to handler first before going to original object. Let's see in below example, how it works:

```javascript
const originalObj = {
    firstName: "Jay",
    lastName: "Prakash"
};
const handler = {
    set: (target, property, value) => {
        if (value.length > 2) {
            target[property] = value;
        } else {
            alert("First Name should be more than 2 characters !");
        }
        return true;
    },
};
const proxyObj = new Proxy(originalObj, handler);
proxyObj.firstName = 'Jo';
```
In above code snippet, what I'm doing is simply defining an object called **_originalObject_**.

Then, I'm defining another object called **_handler_** that contains a trap called **_set_**. We call this trap because this is what it's doing, it is trapping or intercepting the call to original object.

The **_set_** trap will get called when someone tries to set/update the property of **_originalObject_**, in our case, whenever we try to update the **_firstName_** property with value of length 2, it will give an alert message because in our **_set_** trap we are checking if value that we intend to set has length greater than 2 otherwise we are triggering an alert and simply returning from **_set_** trap.

Please note, there are many traps for Proxy object, here we are only using **_set trap_**, for a full list of traps, refer [here](https://www.digitalocean.com/community/tutorials/js-proxy-traps).
---
Now, we know that how JavaScript Proxy object works, so let's start implementing our own two-way data binder with the help of JS proxies.

So, to bind data between model and view, we need to define some nomenclature. We will use **_HTML5 data attribute_** to accomplish this task. Let's say we define **_data-model_** attribute for defining model and **_data-bind_** attribute for defining on which HTML elements, the respective data will be binded. One thing to note here is that values of these data attributes will act like keys to recognize which element has to be updated when it's binded model changes it's state. Let's see in below example what I'm talking about:
```html
<input type="text" data-model="input_1" />
<div data-bind="input_1"></div>
```
In above example, you can see that I have given two attributes **_data-model_** & **_data-bind_** and values of both attributes are same that means **_INPUT_** element will bind to **_DIV_** element, so what we will do is whenever value of **_INPUT_** element containing **_data-model_** changes, we will update the respective element with **_data-bind_** attribute matching the same key, in our case, it is **_input_1_**.

**Let's create a method to update view.**
```javascript
 // Update view when current state of object changes
 const updateView = (obj, prop) => {
     // Get all elements which is binded to the obj prop that got changed
     const elements = document.querySelectorAll(`[data-bind=${prop}]`);

     // Loop through all elements & sync view with the model
     elements.forEach(element => {
         const nodeType = element.nodeName;
         if (nodeType === 'INPUT') {
             element.value = obj[prop];
         } else {
             element.textContent = obj[prop];
         }
     });
 };
```
In above code, we defined one method called **_updateView_** that is receiving two prameters: **_targetObject & the property that needs update_**. What we are doing is we are getting all elements of HTML page that contains **_data-bind_** attribute with the same key that we are receiving as second parameter. Then, we are looping through all such elements that needs an update and we update the value based on the key that we received as second parameter in this method.

**Now, let's write some code to initialize a model and attach handlers to it.**
```javascript
// Define a object to hold the state
const obj = {}; // can be referred as a model to hold all data

// Get all elements which value needs to be updated back to model
const models = document.querySelectorAll('[data-model]');
models.forEach(model => {
    const attributeValue = model.getAttribute('data-model');
    obj[attributeValue] = '';
    // Add event listener on view elements for syncing back to model
    model.addEventListener('input', (evt) => {
        jsBinder[evt.target.dataset.model] = evt.target.value;
    });
});

// Create a proxy & attach handler to it
return new Proxy(obj, {
    set: (target, property, value) => {
        target[property] = value;
        // Sync View With Model
        updateView(target, property);
        return true;
    },
});
```
Now, what we are doing is we are defining a variable called **_obj_** that will act as a model that will hold all model related data based on key.

Then, we are getting all elements from HTML page containing data-model attribute & attaching input event listener to each of those elements and in that listener what we are doing is we are trying to set property through Proxy object based on the key that we are getting from **_evt.target.dataset.model_** which in turn will invoke the **_set trap_** that is defined inside Proxy object. Now, in set trap, what we are doing is simply **_setting/updating_** the property based on whether it is **_new/old_** property and finally we are calling the updateView method that we created earlier to update the view.

Hence, in input event listener, we are syncing our model from view whenever our view changes and in **_set trap_** we are syncing back the view from model by calling **_updateView method_** if someone changes the model itself either programmatically or via some external source.

Huh, that was a lot of code, aggregating all lines of code above, let's see the following code that is whole implementation of our own binder. You can also find whole implementation in this [GitHub repository](https://github.com/jp024556/js-binder).
```javascript
// Initialize the model that will hold the object state
const jsBinder = (() => {

    // Update view when current state of object changes
    const updateView = (obj, prop) => {
        // Get all elements which is binded to the obj prop that got changed
        const elements = document.querySelectorAll(`[data-bind=${prop}]`);
        // Loop through all elements & sync view with the model
        elements.forEach(element => {
            const nodeType = element.nodeName;
            if (nodeType === 'INPUT') {
                element.value = obj[prop];
            } else {
                element.textContent = obj[prop];
            }
        });
    };

    // Define a object to hold the state
    const obj = {};

    // Get all elements which value needs to be updated back to model
    const models = document.querySelectorAll('[data-model]');
    models.forEach(model => {
        const attributeValue = model.getAttribute('data-model');
        obj[attributeValue] = '';
        // Add event listener on view elements for syncing back to model
        model.addEventListener('input', (evt) => {
            jsBinder[evt.target.dataset.model] = evt.target.value;
        });
    });

    // Create a proxy & attach handler to it
    return new Proxy(obj, {
        set: (target, property, value) => {
            target[property] = value;

            // Sync View With Model
            updateView(target, property);
            return true;
        },
    });
})();
```
Final note, please share with your friends if this looks interesting to you, found any mistake, have some suggestions, questions etc, please post a comment I will try my best to answer it.

You can also find this same article published on linkedin [here](https://www.linkedin.com/feed/update/urn:li:ugcPost:6999691216195866624?updateEntityUrn=urn%3Ali%3Afs_updateV2%3A%28urn%3Ali%3AugcPost%3A6999691216195866624%2CFEED_DETAIL%2CEMPTY%2CDEFAULT%2Cfalse%29).

Thanks for reading & have a good day ahead.