Liaison de données bidirectionnelle signifie que lorsque les propriétés d'un objet changent, l'interface utilisateur correspondante peut être modifiée en même temps, et vice versa. En d’autres termes, si nous avons un objet utilisateur doté d’une propriété name, chaque fois que vous définissez une nouvelle valeur sur user.name, l’interface utilisateur affichera la nouvelle valeur. De même, si l'interface utilisateur contient une zone de saisie pour le nom de l'utilisateur, la saisie d'une nouvelle valeur entraînera la modification en conséquence de la propriété de nom de l'objet utilisateur.
De nombreux frameworks javascript populaires, comme Ember.js, Angular.js ou KnockoutJS, font la promotion de la liaison de données bidirectionnelle comme l'une de leurs principales fonctionnalités. Cela ne signifie pas que l’implémenter à partir de zéro est difficile, ni que l’utilisation de ces frameworks est notre seule option lorsque nous avons besoin de cette fonctionnalité. L'idée sous-jacente à l'intérieur est en fait assez basique, et sa mise en œuvre peut être résumée dans les trois points suivants :
Bien qu'il existe de nombreuses façons d'atteindre ces objectifs, un moyen simple et efficace consiste à l'implémenter via le modèle de publication-abonnement. La méthode est simple : nous pouvons utiliser l'attribut de données personnalisé comme attribut à lier dans le code HTML. Tous les objets JavaScript et éléments DOM liés ensemble s'abonneront à cet objet de publication-abonnement. Chaque fois que nous détectons un changement dans un objet Javascript ou un élément d'entrée HTML, nous transmettons le proxy d'événement à l'objet de publication-abonnement, puis transmettons et diffusons toutes les modifications qui se produisent dans les objets et éléments liés via celui-ci.
Un exemple simple implémenté avec jQuery
Il est assez simple et direct d'implémenter ce dont nous avons discuté ci-dessus via jQuery, car en tant que bibliothèque populaire, elle nous permet de nous abonner et de publier facilement des événements DOM, et nous pouvons également en personnaliser un :
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; }
En ce qui concerne les objets JavaScript, voici un exemple d'implémentation minimale d'un modèle de données utilisateur :
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; }
Maintenant, chaque fois que nous voulons lier les propriétés d'un objet à l'interface utilisateur, nous définissons simplement l'attribut de données approprié sur l'élément HTML correspondant.
// javascript var user=new User(123); user.set("name","Wolfgang"); // html <input type="number" data-bind-123="name" />
Les modifications de valeur dans la zone de saisie seront automatiquement mappées à l'attribut de nom de l'utilisateur, et vice versa. Vous avez terminé !
Une implémentation qui ne nécessite pas jQuery
De nos jours, la plupart des projets utilisent généralement jQuery, donc l'exemple ci-dessus est tout à fait acceptable. Mais que se passe-t-il si nous devons être complètement indépendants de jQuery ? Eh bien, en fait, ce n'est pas difficile à faire (surtout lorsque nous ne prenons en charge que IE8 et supérieur). Enfin, il suffit d'observer les événements DOM à travers le modèle publication-abonnement.
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; }
Le modèle de données peut rester inchangé, à l'exception de l'appel à la méthode trigger dans jQuery dans le setter, qui peut être remplacé par notre méthode de publication personnalisée dans 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 ); } } // ... }
Nous l'avons expliqué à travers des exemples et avons une fois de plus obtenu les résultats souhaités avec moins d'une centaine de lignes de JavaScript pur maintenable. Nous espérons que cela sera utile à tout le monde pour réaliser une liaison bidirectionnelle de données JavaScript.