リスナ登録ユーティリティ
Modelの更新をリスンするで作ったリスナ登録ユーテイリティをDIコンテナで使えるようにしてみました。
以下はサンプル。モデル、ビュー、コントローラをDIコンテナに登録して関連づけています。モデル→ビューの関連づけでリスナ登録ユーティリティを利用。
// モデル / 状態を保持するだけのオブジェクト 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( state ) { document.getElementById( this.elm ).innerHTML = state; } } // コンテナ var c = new container.Container( function ( binder ) { // モデル binder.bind("model").to( Kitten ); // コントローラ binder.bind("ctrl").to( KittenService ).inject( { "kitten":container.component("model") } ); // ビュー binder.bind("view1").to( KittenStateView ).inject( {"elm":"stdout1"} ); binder.bind("view2").to( KittenStateView ).inject( {"elm":"stdout2"} ); binder.bind("view3").to( KittenStateView ).inject( {"elm":"stdout3"} ); // 関連づけユーティリティを登録 / EagerSingletonなので登録するだけでOK binder.bind("listener-binder").to( LitenerBinder ); // 関連づけレシピを登録する。LitenerBinderはこれを見てリスナを設定する。 binder.bind("listen@model#state").toFunction( "view1", "onStateChanged" ); binder.bind("listen@model#state").toFunction( "view2", "onStateChanged" ); binder.bind("listen@model#state").toFunction( "view3", "onStateChanged" ); }); // コントローラを作成。 var service = c.get("ctrl"); service.run(); // モデルの更新を受けてビューが更新される。
確認はこちらから。
なかなか疎結合ではないかと。コントローラーでset関数を使わないといけないのがちょっといやな感じですが、まぁ仕方ないよね。
ユーティリティの実装は次の通りです。
/** * コンポーネントのフィールドの変更を捕捉するリスナを設定する。 */ LitenerBinder = function() { this.prefix = "listen"; // @inject } LitenerBinder.prototype = { meta: { "@Container": { "@Scope": container.Scope.EagerSingleton, "@Initialize": function( binder, container ) { binder.bind( container ); } } }, /** * コンポーネント定義を元にモデルのリスナを設定する。 * @param {Object} container */ bind: function( container ) { var thiz = this; container.eachComponentDefs( function( key, list ) { var result = new RegExp( thiz.prefix + "@(.*)#(.*)$").exec( key ); if ( result && result.length >= 3 ) { var component = container.get( result[1] ); var listeners = container.gets( key ); if ( component && listeners && result[2]) { thiz.addListeners( component, result[2], listeners ); } } } ); }, /** * 更新リスナを追加する。 * @param {Object} target リスナを追加するモデル * @param {String} key イベントキー * @param {Function} listener リスナ関数の配列 */ addListeners: function ( target, key, listeners ) { if ( !target.__listeners ) { // ターゲットを拡張する。 target.__listeners = {}; // リスナの記録先を確保。 var org = target.set; // setを上書き。変更を受けてリスナをキックする関数にする。 target.set = function( key, value ) { var result = null; if ( org ) { result = org(); } else { this[key] = value; } if ( this.__listeners[key] ) { for (var i=0; i < this.__listeners[key].length; i++ ) { this.__listeners[key][i]( value ); } } return result; } } // リスナをターゲットの属性として追加。 target.__listeners[key] = listeners; } }