Easy implementation of javascript data two way binding

  • 2020-10-07 18:35:22
  • OfStack

Bidirectional data binding refers to the ability to change the corresponding UI when an object's properties change, and vice versa. In other words, if we have an user object that has an name attribute, whenever you set a new value to user.name, UI will also show the new value. Similarly, if UI contains an input box for the data user name, entering a new value will cause the name attribute of the user object to change accordingly.

Many popular javascript framework, like Ember js, Angular. js or KnockoutJS will two-way data binding as the main characteristics of propaganda. This does not mean that it is difficult to implement from scratch, nor does it mean that using these frameworks is our only option when we need this functionality. The underlying idea inside is actually quite basic and can be realized in the following three ways:

We need a way to determine which UI element is bound to which attribute. We need to monitor the properties and UI changes We need to propagate changes to all the bound objects and UI elements.

Although there are many ways to do this, one simple and efficient way to do it is through the publish subscriber pattern. The approach is simple: We can use the custom data property as the property to bind to in the HTML code. All Javascript objects and DOM elements bound from 1 will subscribe to this publish-and-subscribe object. Whenever we detect a change in either the Javascript object or the input element of HTML, we pass the event broker to the Publish-and-subscribe object, which then passes and broadcasts all changes that occur in the bound object and element.

A simple example implemented with jQuery

Implementing jQuery is fairly straightforward, because as a popular library, it makes it easy for us to subscribe to and publish DOM events, and we can also customize one:


function DataBinder(object_id){
  // Use a jQuery object as simple PubSub
  var pubSub=jQuery({});

  // We expect a `data` element specifying the binding
  // in the form:data-bind-<object_id>="<property_name>"
  var data_attr="bind-"+object_id,
    message=object_id+":change";

  // Listen to chagne events on elements with data-binding attribute and proxy
  // then to the PubSub, so that the change is "broadcasted" to all connected objects
  jQuery(document).on("change","[data-]"+data_attr+"]",function(eve){
    var $input=jQuery(this);

    pubSub.trigger(message,[$input.data(data_attr),$input.val()]);
  });

  // PubSub propagates chagnes to all bound elemetns,setting value of
  // input tags or HTML content of other tags
  pubSub.on(message,function(evt,prop_name,new_val){
    jQuery("[data-"+data_attr+"="+prop_name+"]").each(function(){
      var $bound=jQuery(this);

      if($bound.is("")){
        $bound.val(new_val);
      }else{
        $bound.html(new_val);
      }
    });
  });
  return pubSub;
}

For javascript objects, here is an example of the minimized user data model implementation:


function User(uid){
  var binder=new DataBinder(uid),
    
    user={
      attributes:{},
      // The attribute setter publish changes using the DataBinder PubSub
      set:function(attr_name,val){
        this.attributes[attr_name]=val;
        binder.trigger(uid+":change",[attr_name,val,this]);
      },

      get:function(attr_name){
        return this.attributes[attr_name];
      },
    
      _binder:binder
    };

  // Subscribe to PubSub
  binder.on(uid+":change",function(evt,attr_name,new_val,initiator){
    if(initiator!==user){
      user.set(attr_name,new_val);
    }
  });

  return user;
}

Now, whenever we want to bind an object property to UI, we just set the appropriate data property on the corresponding HTML element.


// javascript 
var user=new User(123);
user.set("name","Wolfgang");

// html
<input type="number" data-bind-123="name" />

Value changes on the input input field are automatically mapped to the name attribute of user and vice versa. There we go!

The jQuery implementation is not required

Most project 1 jQuery is already in use, so the above example is perfectly acceptable. But if we need to be completely independent of jQuery, how do we do that? Well, it's actually not that hard to do (especially if we only support IE above IE8). In the end, we're just going to look at the DOM event by publishing subscriber mode.


function DataBinder( object_id ) {
 // Create a simple PubSub object
 var pubSub = {
  callbacks: {},

  on: function( msg, callback ) {
   this.callbacks[ msg ] = this.callbacks[ msg ] || [];
   this.callbacks[ msg ].push( callback );
  },

  publish: function( msg ) {
   this.callbacks[ msg ] = this.callbacks[ msg ] || []
   for ( var i = 0, len = this.callbacks[ msg ].length; i < len; i++ ) {
    this.callbacks[ msg ][ i ].apply( this, arguments );
   }
  }
 },

 data_attr = "data-bind-" + object_id,
 message = object_id + ":change",

 changeHandler = function( evt ) {
  var target = evt.target || evt.srcElement, // IE8 compatibility
    prop_name = target.getAttribute( data_attr );

  if ( prop_name && prop_name !== "" ) {
   pubSub.publish( message, prop_name, target.value );
  }
 };

 // Listen to change events and proxy to PubSub
 if ( document.addEventListener ) {
  document.addEventListener( "change", changeHandler, false );
 } else {
  // IE8 uses attachEvent instead of addEventListener
  document.attachEvent( "onchange", changeHandler );
 }

 // PubSub propagates changes to all bound elements
 pubSub.on( message, function( evt, prop_name, new_val ) {
var elements = document.querySelectorAll("[" + data_attr + "=" + prop_name + "]"),
  tag_name;

for ( var i = 0, len = elements.length; i < len; i++ ) {
 tag_name = elements[ i ].tagName.toLowerCase();

 if ( tag_name === "input" || tag_name === "textarea" || tag_name === "select" ) {
  elements[ i ].value = new_val;
 } else {
  elements[ i ].innerHTML = new_val;
 }
}
 });

 return pubSub;
}

The data model can remain unchanged, except for the call to the trigger method in jQuery in setter, which we can replace with our custom publish method in PubSub.


// In the model's setter:
function User( uid ) {
 // ...

 user = {
  // ...
  set: function( attr_name, val ) {
     this.attributes[ attr_name ] = val;
     // Use the `publish` method
     binder.publish( uid + ":change", attr_name, val, this );
  }
 }

 // ...
}

Through the example explanation, and once again through 100 lines less, maintainable pure javascript achieved the result we want, I hope to help you to realize javascript data two-way binding.


Related articles: