Backbone.jsはJavaScriptのMVCフレームワークの中では縛りが緩めで柔軟な使い方ができる(らしい)一方、ModelとDOM要素を関連付けて自動的に連動するような便利機能、いわゆるUIバインディングをサポートしていません。Modelが変更された時に発火されるイベント先で自力でDOMに反映させたり、DOMでのイベント発生時にModelに反映させる処理を自力で書く必要があります。
そんなBackbone.jsのUIバインディングライブラリを自作しようと思い情報収集をしていたら、すでに完成度の高いライブラリがあったのでちょっと試してみました。
ダウンロードやその他詳細はこちらの本家でどうぞ。
日本語の情報はほぼなかったので(まあ本家のサンプルコード見るだけで大体分かるっていうのもあると思いますが)、ここでは簡単な使い方とかをメモしておこうと思います。
基本的なサンプル
<div id="app-luke" class="demo">
<label>First:</label>
<input type="text" class="first-name">
<label>Last:</label>
<input type="text" class="last-name">
<b>Full Name:</b>
<span class="first-name"></span>
<span class="last-name"></span>
</div>
姓と名を入力するinput
とその内容をフルネームとして表示するspan
を用意したHTMLです。見た目はこんな感じになります。
JavaScript側で、姓もしくは名が入力されると自動的にフルネームも変更されるように連動させます。
(backbone.jsと依存ライブラリ、およびbackbone.epoxy.jsはロード済みと仮定します)
var bindModel = new Backbone.Model({
firstName: 'Luke',
lastName: 'Skywalker'
});
var BindingView = Backbone.Epoxy.View.extend({
el: '#app-luke',
bindings: {
'input.first-name': "value:firstName,events:['keyup']",
'input.last-name': "value:lastName,events:['keyup']",
'span.first-name': 'text:firstName',
'span.last-name': 'text:lastName'
}
});
var view = new BindingView({model: bindModel});
と、こんなに簡潔に書く事ができます。
素のBackboneとの違いは、Viewの継承元がBackbone.ViewではなくBackbone.Epoxy.Viewとなってる所と、bindingsというプロパティに、ModelとDOM要素を連動させる設定を記述している部分です。eventsプロパティと同じような感覚で書く事ができます。DOM要素名をキーに、反映させるDOM属性と、必要であれば発火イベントを指定します。
bindingsプロパティの簡単な説明
- 1,2行目は姓・名それぞれについて
keyup
イベントが発生したらModelのfirstName・lastNameに自動的に反映される設定です。 - 3,4行目はModelのfirstName・lastNameが変更されたら自動的にフルネーム表示用の
span
のテキストに反映される設定です。
(当然だけど)UIバインディングは双方向で機能
input
への入力のタイミングだけでなく、以下の様にスクリプト内で直接Modelの内容を変更した場合もspan
、input
それぞれが自動で新しい内容に変更されます。
view.model.set('firstName', 'Anakin');
他にも色々な指定が可能
サンプルではtext
およびvalue
のみを例として挙げていますが、jQueryのような形で他にも色々と指定できます。
// HTMLで出力
'span.last-name': 'html:lastName'
// テキストを変更しつつ、文字色を赤にする
'span.last-name': "css{color:'red'},text:lastName"
// テキストとタイトル属性を変更する
'span.last-name': 'attr{title:lastName},text:lastName'
Epoxy.jsを使用しないで書いたらどうなるの?
ちなみにEpoxy.jsを使用しない、素のBackboneで書く場合のViewはこんな感じになります。
var BindingView = Backbone.View.extend({
el: '#app-luke',
events: {
'keyup input.first-name': 'changeFirstName',
'keyup input.last-name': 'changeLastName',
},
initialize: function () {
this.model
.bind('change:firstName', this.updateFirstName)
.bind('change:lastName', this.updateLastName)
.trigger('change:firstName')
.trigger('change:lastName');
},
changeFirstName: function (e) {
this.model.set('firstName', $(e.target).val());
},
updateFirstName: function (e) {
$('input.first-name').val(this.get('firstName'));
$('span.first-name').text(this.get('firstName'));
},
changeLastName: function (e) {
this.model.set('lastName', $(e.target).val());
},
updateLastName: function (e) {
$('input.last-name').val(this.get('lastName'));
$('span.last-name').text(this.get('lastName'));
}
});
※Epoxy.js使用バージョンと同機能で正確に対比させるためにfirstNameとlastNameのイベントをあえて分けるような冗長的な書き方をしています。
簡単なサンプルレベルでここまで違いがでるので、ちゃんとしたアプリケーションレベルになるとさらに恩恵にあずかれるんじゃないかと思います。
Knockout.js風のUIバインディング
Knockout.jsのように、HTMLタグにインラインでバインディングの設定を記述する方法も可能です。
<div id="app-luke" class="demo">
<label>First:</label>
<input type="text" data-bind="value:firstName,events:['keyup']">
<label>Last:</label>
<input type="text" data-bind="value:lastName,events:['keyup']">
<b>Full Name:</b>
<span data-bind="text:firstName"></span>
<span data-bind="text:lastName"></span>
</div>
var bindModel = new Backbone.Model({
firstName: 'Luke',
lastName: 'Skywalker'
});
var BindingView = Backbone.Epoxy.View.extend({
el: '#app-luke',
bindings: 'data-bind'
});
var view = new BindingView({model: bindModel});
ますます簡潔になりました。HTMLの各タグにdata-bind
という属性でバインディングの設定を書き、スクリプト側ではbindingsプロパティにバインディングする属性名(ここではdata-bind
)をセットするだけです。
また、この方法だとHTMLの各タグ全部にわざわざクラス名のような識別情報を付ける必要がないという利点もあります。
ちなみにKnockout.jsには詳しくないので、このインラインバインディングがKnockout.jsの仕様と互換性があるかどうかまでは分かりませんでした。見た感じだとなさそうですけど。
Modelの値を動的に加工して出力
大変便利なUIバインディングですが、DOMに反映させるModelの値を加工したいからといって下記のような書き方はできません。
var BindingView = Backbone.Epoxy.View.extend({
el: '#app-luke',
bindings: {
'input.first-name': "value:firstName,events:['keyup']",
'input.last-name': "value:lastName,events:['keyup']",
'span.first-name': "html:'<b>' + firstName + '</b>'", // ← 太字にして出力しようとしている(無理)
'span.last-name': 'text:lastName'
}
});
Epoxy.jsでは、こういった加工をするためにcomputedsプロパティというものが用意されています。Modelには存在しないが出力時にだけ動的に作成される仮想的な値を作成する事ができます。SQLでいえばVIEWみたいな感じの機能です。
var BindingView = Backbone.Epoxy.View.extend({
el: '#app-luke',
bindings: {
'input.first-name': "value:firstName,events:['keyup']",
'input.last-name': "value:lastName,events:['keyup']",
'span.first-name': 'html:displayFirstName', // ← displayFirstNameというModelには存在しない名前を指定
'span.last-name': 'text:lastName'
},
computeds: {
// displayFirstNameを動的に作成
displayFirstName: function() {
return '<b>' + this.getBinding('firstName') + '</b>';
}
}
});
唐突にまとめ(メンドくさくなったわけではナイヨ)
軽くさわってみた感想ですが、UIバインディングにありがちな融通の利かなささなんかにもcomputedsのような方法で対処できたり、ここで紹介した内容の他にもView Binding FiltersやCustom Binding Handlersなどの機能があったり、Model自体もcomputedsプロパティで拡張することができるBackbone.Epoxy.Modelなど、まだまだ紹介しきれない機能が盛り沢山でかなり良さ気な感じでもっと色々と紹介したいところなのですが、いい加減長くなってきたのでここらで〆させてもらいます。
続きはWEBで!