Two-way data binding refers to the ability to bind changes in object properties to changes in the user interface, and vice versa. In other words, if we have a user object and a name attribute, once we assign a new value to user.name, the new name will be displayed on the UI. Likewise, if the UI contains an input box for the user's name, entering a new value should cause the user object's name property to change accordingly.
Many popular JS framework clients such as Ember.js, Angular.js or KnockoutJS have implemented two-way data binding in their latest features. This does not mean that it is difficult to implement it from scratch, nor does it mean that adopting these frameworks is the only option when these functions are needed. The idea below is actually very basic and can be thought of as a 3-step plan:
We need a way to bind UI elements and attributes to each other
We need to monitor changes in properties and UI elements
We need to make all bound objects and elements aware of changes
There are still many ways to implement the above idea. One simple and effective way is to use the PubSub mode. The idea is simple: we use data attributes to bind HTML code, and all JavaScript objects and DOM elements that are bound together subscribe to a PubSub object. As long as a JavaScript object or an HTML input element listens to data changes, the event bound to the PubSub object will be triggered, and other bound objects and elements will make corresponding changes.
Use jQuery to make a simple implementation
For subscribing and publishing DOM events, it is very simple to implement it with jQuery. Next, we will use Jquery, such as the following:
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, the following is the simplest implementation method of a 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 attributes to the UI, we only need to bind the appropriate data attributes to the corresponding HTML elements.
// javascript var user = new User( 123 ); user.set( "name", "Wolfgang" ); // html <input type="number" data-bind-123="name" />
In this way, the input value will be automatically mapped to the name attribute of the user object, and vice versa
Same. This simple implementation is complete!
No need for jQuery implementation
In most projects today, jQuery is probably already used, so the above example is completely acceptable. However, what if we need to try going to the other extreme and also remove the dependency on jQuery? Well, it's not that hard to prove (especially since we restrict support to IE 8 and above). Ultimately, we have to implement a custom PubSub using normal javascript and retain DOM events:
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 the same except for calling the jQuery trigger method in the setter. Calling the trigger method will be replaced by calling the publish method of 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 ); } } // ... }
We once again achieved the results we wanted with less than a hundred lines of maintainable pure JavaScript.
The above content is a tutorial on js two-way data binding. I hope it will be helpful to everyone.