【spine.js】Spineの使い方
公開日:
:
spine.js
Spine.jsを使ってみようということで基本となる使い方をメモ。
詳しい話はAPIがあるしチュートリアルもあるのですが、行間を読まないとな部分があるので、初歩の定石みたいなものとしてまとめます。
ちなみにSpineの作者が書いたこの本も参考にしないと、ウェブのドキュメントだけでは私には理解できませんでした。業界が流動的なもんで既に仕様の一部が更新されているようですが、仕組みの部分やjavascriptの基本的な技術の部分は参考になります。
インポート
最低限必要なのはjquery.jsとspine.jsですが、通常テンプレートをことになるのでjquery.tmpl.jsもインポートします。
今回の独自アプリケーションはmyapp.jsとしました。
また、spineのパッケージの中には様々なオプションライブラリが含まれています。例えばHTML5ローカルストレージを使用する場合はlocal.jsを追加します。
もともと容量が少ないので全てspine.jsに含んでくれればいいのに。
サンプルなどにはjson2.jsもインポートされていますが、これはJSONをサポートしていない古いブラウザ(IE7など)をサポートするためのものです。サポートする必要がなければインポートはいりません。
<script src="lib/jquery.js" type="text/javascript" charset="utf-8"></script> <script src="lib/jquery.tmpl.js" type="text/javascript" charset="utf-8"></script> <script src="lib/spine.js" type="text/javascript" charset="utf-8"></script> <script src="lib/myapp.js" type="text/javascript" charset="utf-8"></script> <!-- option --> <script src="lib/local.js" type="text/javascript" charset="utf-8"></script>
Spineでやること
◯いろいろできるとは思いますが、MVCを実践したアプリということで、このエントリでは下記を行います。
・データ(モデル)の定義
・DOMイベントをデータに結びつける
・データの変更をビューに結びつける
・ビューを描画して変更を反映する
これにデータの永続化を加えることでアプリの基本的な操作の要件はみたせるのではないでしょうか。
上記を実現した後は、大規模なアプリ開発によって肥大化するコードをどう管理するかという話になってきそうですが、そのあたりはHemとかCoffeeScriptとか依存性管理とかになってきそうなので、今後の課題としていずれ記事にしたいところです。
◯アーキテクチャ
と、いうほどのことではないかもしれませんが、コントローラやモデルの使い方として1まとまりのデータ操作に対して、1つのコントローラを定義していきます。例えば、デモアプリとして用意されている連絡先管理アプリでは、サイドバーとコンテンツでそれぞれコントローラを持っています。ToDoリストのデモではリストの1項目を管理するコントローラを作成しています。連絡先管理の例では、最後にそれらをまとめるためのAppコントローラを作成しています。
(Appコントローラはinitで各コントローラの初期化のみを行い、他の関数は定義されていません。初期化だけを行うのであれば、べたうちすればよいのでこのコントローラの役割は私にはよくわかりませんでした。もう少し複雑になると、各コントローラの連携のために有効に働くのかもしれません。)
モデルを定義
・ モデルのインスタンスは1レコードを表しています。
・ 定義したモデルクラスは生成したインスタンスのコレクションになっています。
select()やeach()などのクラスメソッドもっていて、生成したインスタンスにアクセス可能です。
・ saveやcreate、destroyなどはイベントであり、同名のメソッドでもある。
//モデルを継承してモデルクラスを定義 var MyModel = Spine.Model.sub(); //スキーマを作成 configure(modelName, attributes...) MyModel.configure("MyModel", "prop1", "prop2"); //HTML5ローカルストレージを使用する場合はLocalを継承 Task.extend(Spine.Model.Local); //独自クラスメソッドを追加 MyModel.exted({ classMethod1 : function(){}, classMethod2 : function(){} }); //作成、プロパティへのアクセス var item = new MyModel(); item.porp1 = "value1";
コントローラの定義
◯Controllerの仕事
コントローラはモデルとビューを結びつける役割があります。
・DOMイベントを拾ってモデルをCRUD処理する
・モデルのCRUDイベントを拾ってDOMに反映させる
・ビューの描画
結局は「DOMイベント→モデル処理→DOMの再描画」なのですが、上記のように2段階に分離しましょうということ。
◯コントローラは自身を表現するDOM要素に関連付けられます
ビューの描画はその要素に対して行います。関連付けは次のように行われます。
・描画済みのDOM要素を渡してインスタンス化する
app = new TaskApp( {el: $(“#tasks”)} );
コンストラクタに渡したオブジェクトはコントローラのプロパティに設定されます。
elは予約されたプロパティで自身のDOM要素へのショートカットです。elに$(“#tasks”)を上がいて設定します。
・未描画のDOM要素を使用する
デフォルトでdiv要素をelに持っています。
app = new TaskApp();
assertEqual(app.el.get(0).tagName, “div”);
メソッド内でthis.html(“hello world”);などとして、コントローラのelをDOM要素に追加します。
$(“body”).append(app.el);
//関係するモデル名を含んだ名前をキャメルケースでつける var MyModels = Spine.Controller.sub({ //html要素のイベントにリスナをバインド {"イベント セレクタ" : "関数", ...} events: { "change input[type=checkbox]": "toggle", "click .destroy": "destroyItem", "dblclick": "edit" }, //html要素を変数に設定 {"セレクタ" : "変数名", ...} elements: { ".items": "items", "form input": "input" }, //コンストラクタ モデルのイベントに関数をバインド init: function(){ this.item.bind("update", this.proxy(this.render)); this.item.bind("destroy", this.proxy(this.remove)); }, //viewを描画 render: function(){ this.el.html($("#taskTemplate").tmpl(this.item)); }, });
CRUDとSelect
リストの処理としてCRUDと選択を行います。
・CreateとReadはリスト全体に対して
・UpdateとDeleteとSelectは項目に対して
行われるので、CRとUDSでコントローラを分けて実装します。
var TaskApp = Spine.Controller.sub({ /************************************************************************************************** * 変数を定義 **************************************************************************************************/ //html要素を変数に設定 {"セレクタ" : "変数名", ...} elements: { ".items": "items", "form input": "input" }, selected: "", /************************************************************************************************** * Viewにリスナをバインド **************************************************************************************************/ //html要素のイベントにリスナをバインド {"イベント セレクタ" : "関数", ...} events: { "submit form": "createModel", "click .item": "selectModel", "click input[type=checkbox]": "updateModel", "click .destroy": "deleteModel", }, createModel: function(e) { this.log("create"); e.preventDefault(); Task.create({name: this.input.val()}); this.input.val(""); }, selectModel : function(e){ console.log("selectModel1"); var task; if( this.selected ){ //削除された場合はスルー try{ task = Task.find( this.selected ); task.selected = false; task.save(); }catch(e){ } } var newId = $(e.currentTarget).attr("id"); this.selected = newId; task = getModelByElem(e.currentTarget, Task); task.selected = true; task.save(); this.selected = task.id; }, deleteModel: function(e){ var task = getModelByElem(e.currentTarget, Task); this.log("deleteModel"); task.destroy(); e.stopPropagation(); }, updateModel : function(e){ this.log("toggle"); var task = getModelByElem(e.currentTarget, Task); task.done = !task.done; task.save(); e.stopPropagation(); }, /************************************************************************************************** * Modelにリスナをバインド **************************************************************************************************/ //コンストラクタ モデルのイベントに関数をバインド init: function(){ this.log("TaskApp init"); Task.bind("refresh", this.proxy(this.listAll));//fetch()実行時に発生 Task.bind("create", this.proxy(this.createView)); Task.bind("update", this.proxy(this.updateView)); Task.bind("destroy", this.proxy(this.deleteView)); Task.fetch(); }, //レコードのコールバックにはそのレコード(task)が渡される。 listAll: function(){ this.log("listAll"); Task.each(this.proxy(this.createView)); }, createView: function(model){ this.log("createView"); model.selected = false; this.items.append(this.render(model)); }, updateView : function(model){ console.log("updateView"); $("#"+model.id).replaceWith( this.render(model) ); }, deleteView: function(model){ $("#"+model.id).remove(); }, //リスナを定義 render: function( model ){ this.log("render"); return $("#taskTemplate").tmpl(model); }, });
DOM要素からモデルインスタンスを取得するためのユーティリティ
/* * レコードのルート要素のクラスにrootを設定した状態で実行 * elem : レコードの子DOM要素 * Model : 対応するModelクラス * return : 対応するModelインスタンス */ function getModelByElem(elem, Model){ var model; var jQchild = $(elem); var id; if( jQchild.hasClass("root") ){ id = jQchild.attr("id"); }else{ var parents = jQchild.parents(); for( i=0 ; i<parents.length; ++i){ var jqobj = parents.eq(i); if( jqobj.hasClass("root") ){ id = jqobj.attr("id"); break; } } } model = Model.find(id); return model; }
上記ユーティリティーを使う場合はテンプレートは下記のようにルート要素のクラスにrootを与えてやります。
<script type="text/x-jquery-tmpl" id="taskTemplate"> <div class="root item {{if done}}done{{/if}} {{if selected}}selected{{/if}}" id="${id}"> <div class="view" title="Double click to edit..." style="background: #F0F000"> <input type="checkbox" {{if done}}checked="checked"{{/if}}> <span>${name}</span> <a class="destroy"></a> </div> <div class="edit" style="background: #F0F0F0"> <input type="text" value="${name}"> </div> </div> </script>
関連記事
-
-
【spine.js】Spine.jsの基本 ~クラスの操作~
このエントリでprivateなプロパティを宣言してクラスみたいなことをやっていたのだけれど、これだけ
-
-
【spine.js】Spineのドキュメント翻訳 Models
元のドキュメントはこちら はじめに 状態が変化する中、クライアントサイドで実現したい
-
-
【spine.js】Spine.jsを使ってはまったポイント
Spine.jsでクラスの継承をさせようとしていくつかはまってしましました。 ※CoffeSc
-
-
【spine.js】Spineのドキュメント翻訳 Views & Templating using jQuery.tmpl
javascriptのテンプレートエンジンは他にMustacheというのが有力そうです。強みは様々な
-
-
【spine.js】Spineのドキュメント翻訳 Routing
元のドキュメントはこちら ↓↓↓ Routing - Documentation - Spi
-
-
【spine.js】Spineのドキュメント翻訳 Controllers
元記事はこちら はじめに controllerはSpineの三位一体の最後の1つ