双方向データ バインディング は、オブジェクトのプロパティが変更されると、対応する UI も同時に変更でき、またその逆も可能であることを意味します。つまり、name プロパティを持つユーザー オブジェクトがある場合、新しい値を user.name に設定するたびに、UI には新しい値が表示されます。同様に、UI にユーザー名の入力ボックスが含まれている場合、新しい値を入力すると、それに応じてユーザー オブジェクトの name プロパティが変更されます。
Ember.js、Angular.js、KnockoutJS などの多くの人気のある JavaScript フレームワークは、主な機能の 1 つとして双方向のデータ バインディングを推進しています。これは、最初から実装することが難しいという意味でも、この機能が必要な場合にこれらのフレームワークを使用することが唯一の選択肢であるという意味でもありません。内部の基本的なアイデアは実際には非常に基本的なもので、その実装は次の 3 つの点に要約できます:
これらの点を達成するには多くの方法がありますが、シンプルで効率的な方法は、パブリッシュ/サブスクライバー パターンを通じて実装することです。方法は簡単です。HTML コードでバインドする必要がある属性として、カスタマイズされたデータ属性を使用できます。一緒にバインドされているすべての JavaScript オブジェクトと DOM 要素は、このパブリッシュ/サブスクライブ オブジェクトにサブスクライブします。 Javascript オブジェクトまたは HTML 入力要素のいずれかで変更が検出されるたびに、イベント プロキシをパブリッシュ/サブスクライブ オブジェクトに渡し、バインドされたオブジェクトと要素で発生するすべての変更を渡してブロードキャストします。
jQuery で実装された簡単な例
jQuery を使用して上で説明した内容を実装するのは非常にシンプルで簡単です。人気のあるライブラリなので、DOM イベントを簡単にサブスクライブして発行でき、カスタマイズすることもできます。
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; }
JavaScript オブジェクトに関しては、最小限のユーザー データ モデル実装の例を次に示します。
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; }
オブジェクトのプロパティを UI にバインドする場合は、対応する HTML 要素に適切なデータ属性を設定するだけです。
// javascript var user=new User(123); user.set("name","Wolfgang"); // html <input type="number" data-bind-123="name" />
入力ボックス内の値の変更はユーザーの名前属性に自動的にマッピングされ、その逆も同様です。これで完了です。
jQuery を必要としない実装
最近のほとんどのプロジェクトは一般に jQuery を使用するため、上記の例は完全に許容されます。しかし、jQuery から完全に独立する必要がある場合はどうすればよいでしょうか?実際、これを行うのは難しくありません (特に IE8 以降のみのサポートを提供する場合)。最後に、パブリッシュ/サブスクライバー パターンを通じて DOM イベントを観察する必要があります。
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; }
データ モデルは、セッター内の jQuery のトリガー メソッドの呼び出しを除き、変更しないままにすることができます。これは、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 ); } } // ... }
例を通して説明しましたが、ここでもまた、保守可能な純粋な JavaScript の 100 行未満で、私たちが望んでいた結果を達成できました。これが、JavaScript データの双方向バインディングを実現する際に役立つことを願っています。