Step by Step implementing Two-Way Data Binding in Vanilla JavaScript

Data Binding is one of the most important features of all the frameworks. It ensures that the data model and the views are in sync with each other. It is very fundamental feature of any MV* frameworks such that Model-View-Controller, Mode-View-ViewModel, and Model-View-Presenter, etc.

Popular JavaScript based frameworks such that React, Angular have their way to implement it. However, it is a good idea to know how a basic two-way data binding can be implemented using vanilla JavaScript.  That would give you an overview of how much heavy lifting is done by the framework when you use features like data binding.

To get most of this post, you should have a basic understanding of following JavaScript features

  • Creating an object as literal
  • Adding a property with Object.defineProperty
  • Using setter and getter
  • forEach statement

 

You can refresh about JavaScript object creation in this video on geek97. Let us get started,

On the HTML, we have put one input text box and two span elements.  Whenever the user enters a value in the text box, data in <span> element should get updated.


<body>
<h2>Two-way data binding</h2>
Name: <input data-geek97-bind="name" type="text">
<span data-geek97-bind="name"> </span>
<span data-geek97-bind="name"> </span>
<script src="databinding.js"></script>
</body>

view raw

db1.html

hosted with ❤ by GitHub

We are going to write JavaScript for data binding in the databinding.js file, so we also added a reference to that in the HTML.

To start with databinding.js file contains an Immediately Invoked Function Expression (IIFE). We write all the codes inside this function,


(function (){
console.log('Implementing two way data binding');
})();

view raw

cdjs2.js

hosted with ❤ by GitHub

At this point, on launching the application, you should get below output. Right now, if you enter anything in the Name field nothing would be affected.

cd1

Note: If you are using Visual Studio Code, install the extension Live Server, and run the HTML in the browser, right click on the file and from the context menu select “Open with Live Server” to launch the application.

Our first task is to select all the elements which have [data-geek97-bind] attribute set, and also check for their type. Right now, as we are creating binding only for input type text, so we can make a selection of elements and type checking as shown below,


var elements = document.querySelectorAll('[data-geek97-bind]');
var dbrepo = {};
elements.forEach((element)=>{
if(element.type === 'text'){
let bindingProperty = element.getAttribute('data-geek97-bind');
console.log(bindingProperty);
}
});

view raw

cdjs3.js

hosted with ❤ by GitHub

We have created an empty object called dbrepo, which would maintain the state between the data bound elements. The dbrepo object would make sure that all DOM elements with [data-geek97-bind] attribute set to value ‘name’ is in sync and have the same value.

To achieve that, what all we need to do is to add a property dynamically to the dbrepo object for each unique value of [data-geek97-bind] attribute in the DOM elements. For that, let us create a function called addToScope. In the function, we check whether a particular property exists or not, if not then using the Object.defineProperty() method adds a property to the dbrepo object.


function addToScope(){
if(!dbrepo.hasOwnProperty(bindingProperty)){
let value;
Object.defineProperty(dbrepo,bindingProperty,{
configurable:true,
enumerable:true ,
set: function(newvalue){
value = newvalue;
},
get:function(){
return value;
}
})
}
}

view raw

cdjs5.js

hosted with ❤ by GitHub

Using the hasOwnProperty() method, determining whether property already exists or not, if not then add it using the Object.defineProperty() method. You can learn more about property descriptor and Object.defineProperty() here

We need to do some work in the setter function.

  • First, find all the elements with data-geek97-bind attribute set to the same value.
  • If the element type is text, set its value, otherwise set innerHtml of the element

Above two tasks can be performed in the setter function as below,


set: function (newvalue) {
value = newvalue;
elements.forEach(e => {
if (e.getAttribute('data-geek97-bind') === bindingProperty) {
if (e.type === 'text') {
e.value = newvalue
}
else if (!e.type) {
e.innerHTML = newvalue
}
}
})
},

view raw

cdjs6.js

hosted with ❤ by GitHub

Putting everything together, code to perform two-way data binding is as below:


var elements = document.querySelectorAll('[data-geek97-bind]');
var dbrepo = {};
elements.forEach((element) => {
if (element.type === 'text') {
var bindingProperty = element.getAttribute('data-geek97-bind');
addToScope();
element.onkeyup = () => {
dbrepo[bindingProperty] = element.value;
}
}
function addToScope() {
if (!dbrepo.hasOwnProperty(bindingProperty)) {
let value;
Object.defineProperty(dbrepo, bindingProperty, {
configurable: true,
enumerable: true,
set: function (newvalue) {
value = newvalue;
elements.forEach(e => {
if (e.getAttribute('data-geek97-bind') === bindingProperty) {
if (e.type === 'text') {
e.value = newvalue
}
else if (!e.type) {
e.innerHTML = newvalue
}
}
})
},
get: function () {
return value;
}
})
}
}
});

view raw

cdjs7.js

hosted with ❤ by GitHub

If you want to change the value in the code by just rewriting the value of the property in the dbrepo object,


changeName = function () {
dbrepo.name = 'set by code ';
}

view raw

cdjs9.js

hosted with ❤ by GitHub

At this point, on launching the application you should get below output. Right now, if you enter anything in the Name field, the other two spans would be affected.

cd2

Still, there is scope for much improvement in this code,

  1. Instead of rewriting the innerHtml, append the next text
  2. Extend it for other types than text.

I hope you find this post useful. Thanks for reading.

 

Leave a comment