リスナ登録ユーティリティ for ContainerJS 0.3.0
昔書いたリスナ登録ユーティリティをContainerJS version 0.3.0にあわせて修正。Typeを使ってちょっと短く書けるようになってます。
サンプル
簡単に解説。
- モデル、ビュー、コントローラからなる簡単なプログラムです。
- ビュー→モデルの依存でオブザーバーパターンを採用。リスナ登録ユーティリティを使ってリスナ(オブザーバー)をモデルに関連付けています。
// モデル / 状態を保持するだけのオブジェクト function Kitten( ) { this.state = "stop"; } // コントローラー // API呼び出しを受けてモデルを更新する。 function KittenService( kitten ) { this.kitten = kitten; } KittenService.prototype = { run: function() { this.kitten.set("state", "running"); // set関数で更新するルール }, stop: function() { this.kitten.set("state", "stop"); }, jump: function() { this.kitten.set("state", "jump!"); } } // ビュー / モデルの更新を受けてUIを更新する。 function KittenStateView( elm ) { this.elm = elm; } KittenStateView.prototype = { // 状態変更ハンドラ onStateChanged: function( e ) { document.getElementById( this.elm ).innerHTML = e.value; } } // コンテナ var c = new container.Container( function ( binder ) { // モデル binder.bind( Kitten ).to( "model" ); // コントローラ binder.bind( KittenService ).to( "ctrl" ).inject( { "kitten":container.component("model") } ); // ビュー binder.bind( KittenStateView ).inject( {"elm":"stdout1"} ); binder.bind( KittenStateView ).inject( {"elm":"stdout2"} ); binder.bind( KittenStateView ).inject( {"elm":"stdout3"} ); // リスナ登録ユーティリティ / EagerSingletonなので登録するだけでOK binder.bind( container.utils.ListenerBinder ).inject({ // 監査対象とリスナを指定 // "model"コンポーネントの"state"の変更を"onStateChanged"を持つコンポーネントに通知する。 recipe : { "model#state" : "onStateChanged" } }); }); // コントローラを作成。 var service = c.get("ctrl"); service.run(); // モデルの更新を受けてビューが更新される。
実装
ユーティリティの実装は次の通りです。
if(typeof container=="undefined"){ container={} } if(typeof container.utils=="undefined"){ container.utils={} } /** * コンポーネントのフィールドの変更を捕捉するリスナを設定する。 */ container.utils.ListenerBinder = function() { this.recipe = {}; // @Inject } container.utils.ListenerBinder.prototype = { meta: { "@Container": { "@Scope": container.Scope.EagerSingleton, "@Initialize": function( binder, container ) { binder.bind( container ); } } }, /** * コンポーネント定義を元にモデルのリスナを設定する。 * @param {Object} c コンテナ */ bind: function( c ) { for ( var i in this.recipe ) { if ( typeof i == "function" ) { continue; } var result = new RegExp( "(.*)#(.*)$").exec( i ); if ( result && result.length >= 3 ) { var fn = this.recipe[i]; var component = c.get( result[1] ); var listeners = c.gets( container.types.has( fn ) ); if ( component && listeners && result[2]) { this.addListeners( component, result[2], listeners, fn ); } } } }, /** * 更新リスナを追加する。 * @param {Object} target リスナを追加するモデル * @param {String} key イベントキー * @param {Function} listener リスナ関数の配列 * @param {String} functionName 関数名 */ addListeners: function ( target, key, listeners, functionName ) { if ( !target.__listeners ) { // ターゲットを拡張する。 target.__listeners = {}; // リスナの記録先を確保。 var org = target.set; // setを上書き。変更を受けてリスナをキックする関数にする。 target.set = function( key, value, opt ) { opt = opt ? opt : {}; opt.value = value; var result = null; if ( org ) { result = org(); } else { this[key] = value; } if ( this.__listeners[key] ) { var fn = this.__listeners[key]["function"]; for (var i=0; i < this.__listeners[key]["lis"].length; i++ ) { this.__listeners[key]["lis"][i][fn]( opt ); } } return result; } } // リスナをターゲットの属性として追加。 target.__listeners[key] = {"lis":listeners, "function": functionName}; } }
Listenerの綴り間違いは、さりげなく直しておきました。
それにしても
しばらくさわってないと見事に忘れてる・・orz。container.typesとか忘却の彼方。やばし。