Implement very simple js bidirectional data binding

  • 2020-10-07 18:32:17
  • OfStack

Bidirectional data binding refers to the ability to bind object properties to changes in the user interface and vice versa. In other words, if we have an user object and an name attribute, once we assign a new value to user.name, the new name will be displayed on UI. Similarly, if UI contains an input box for the user name, entering a new value should change the name attribute of the user object accordingly.

Many popular JS framework clients, such as ES12en.js, Angular.js, or KnockoutJS, publish bi-directional data binding in the latest feature. This does not mean that it is difficult to implement from zero, nor does it mean that adopting these frameworks is the only option when these features are needed. The following idea is actually quite basic and can be considered a 3-step plan:

We need a method for binding UI elements and attributes to each other
We need to monitor the changes to the attributes and UI elements
We need to make all the bound objects and elements aware of changes

There are many ways to implement the above idea, but one simple and effective way is to use the PubSub pattern. The idea is simple: we use the data feature to bind the HTML code, and all JavaScript objects and DOM elements bound from 1 will subscribe to 1 PubSub object. Whenever an JavaScript object or an HTML input element listens to a change in the data, an event bound to the PubSub object is triggered so that the other bound objects and elements change accordingly.

Do a simple implementation with jQuery

For DOM event subscription and publishing, jQuery is very easy to implement. Next we use Jquery such as:


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 change events on elements with the data-binding attribute and proxy
 // them to the PubSub, so that the change is "broadcasted" to all connected objects
 jQuery( document ).on( "change", "[data-" + data_attr + "]", function( evt ) {
 var $input = jQuery( this );
 pubSub.trigger( message, [ $input.data( data_attr ), $input.val() ] );
 });
 // PubSub propagates changes to all bound elements, 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("input, textarea, select") ) {
  $bound.val( new_val );
  } else {
  $bound.html( new_val );
  }
 });
 });
 return pubSub;
}

For the above implementation, here is the simplest way to implement an User model:


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 the PubSub
 binder.on( uid + ":change", function( evt, attr_name, new_val, initiator ) {
 if ( initiator !== user ) {
  user.set( attr_name, new_val );
 }
 });
 return user;
}

Now if we want to bind the User model attribute to UI, we just need to bind the appropriate data attribute to the corresponding HTML element.


// javascript
var user = new User( 123 );
user.set( "name", "Wolfgang" );
// html
<input type="number" data-bind-123="name" />

This will automatically map the input value to the name property of the user object and vice versa

Versa. This simple implementation is done!

No implementation of jQuery is required

In most projects today, jQuery is probably already in use, so the above example is perfectly acceptable. However, what if we need to try to go to the other extreme and also remove the dependency on jQuery? Well, it's not hard to verify that 1 (especially if we're limiting support to IE 8 and above). In the end, we had to implement a custom PubSub using 1-like javascript and keep the DOM event:


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 = , 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, // IE 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 {
 // IE 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 = , 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 model can remain one apart from the trigger method called in the setter to jQuery. Calling the trigger method will replace the publish method for calling our customized PubSub with different characteristics:


// 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 );
 }
 }
 // ...
}

Once again we achieved our desired results with the less than 100 lines maintainable pure javascript.

The above is about js bidirectional data binding related tutorial, I hope to help you learn.


Related articles: