jekylog

All doggs wanna be a Geek

初心者のためのSpine.js その1

多分コードも説明もおかしいのであんま参考にしないで下さい。あくまでも初心者である自分の勉強用なので。

ステートフルJavaScriptを写経しながら何となくMVC(MVVCとかMVPとかまだなんのこっちゃ分からん)っぽい概念は分かったけどいざ自分でコード書くとなるといまいちフローが分かないので、汚コードだろうがなんだろうがとにかく自分で考えながらコード書いてみた。徐々に機能を追加、リファクタリングしていく。

ちなみにどうしても自分で考えても分からなくなった時には下記ソースを見ながら進めた。

1. HTMLを用意する

JavaScriptありきのHTMLってのはないと思うのでまずは兎にも角にもHTMLを用意。これを元に作っていく。 最新のSpine.jsも読み込んでおく。 また、jQueryも最新版を使用する。

<div id="mod-app">
    <header>
        <h1>Todoアプリ</h1>
    </header>
    <div id="mod-regist">
        <input type="text" placeholder="例:宿題をやる"><button>create</button>
    </div>
    <div id="mod-tasks">
        <ul></ul>
    </div>
</div>

2. モデルを用意する

ローカルストレージを使用してデータを永続化して使用する前提なのでlocal.jsを読み込む。 ここにユーザーが入力したデータを内部的にもローカルストレージにも保存する。

var Task = Spine.Model.sub();
Task.configure('Task', 'task');
Task.extend(Spine.Model.Local);

3. モデルをハンドリングするコントローラを用意する

ユーザーのデータをモデルに追加したりモデルに変化があった場合にビューに反映したりするコントローラを用意する。名前はモデルの名前を複数形にするのが通例っぽいのでTasksにした。

var Tasks = Spine.Controller.sub();

4. アプリ全体をハンドリングする?コントローラを起動する

ビューごとにコントローラを1つずつ用意するというのが、よく使われているパターンです。

と、ステートフルJavaScriptにあるのでTasksコントローラが個々のタスクのビューを管理するものならば、Appコントローラ(名前はTaskAppとかのほうがよかったかも)は全体のビューを管理するものって考えで良いのだろうか?ちょっとこの辺りまだモヤモヤしてる。

var App = Spine.Controller.sub();

5. DOM構築後にAppコントローラをインスタンス化

コントローラはDOM要素を利用するのでDOM構築後にインスタンス化。

$(function(){
    var app = new App();
});

6. Appコントローラのインスタンス化時にel要素を渡す

どのDOM要素をコントローラで管理?するか示すためel要素を指定。 ちなみにnewするとinitが叩かれるので初期化処理はAppコントローラのinitに記述する。

$(function(){
    var app = new App({
        el: $('#mod-app')
    });
});

7. イベントの委譲

テキストフィールドに文字が入力されており(空の場合はreturn)、createボタンが押下されればcreateメソッドを叩く。 ちなみにプロパティとしてアクセス出来るようにelementsにinputを設定しておく。

var App = Spine.Controller.sub({
    elements: {
        '#mod-regist input': 'input'
    },
    events: {
        'click button': 'create'
    },
    create: function(){
        if(!this.input.val()) return;
    }
});

8. 新規モデルを作成

createボタンが押されればモデルに新規レコードを作成して欲しいのでcreateメソッド内で新規レコードを作成。 また、作成時にtaskプロパティにテキストフィールドの値を渡す。

var App = Spine.Controller.sub({
    elements: {
        '#mod-regist input': 'input'
    },
    events: {
        'click button': 'create'
    },
    create: function(){
        if(!this.input.val()) return;
        var task = Task.create({
            task: this.input.val()
        });
    }
});

9. バインディング

レコードが作成(create)されればcreateイベントが発行されるのでその際の処理(ビューをレンダリング)をインスタンス化時にモデルに対して登録しておく。 ちなみにモデル(Task)からメソッドを叩くのでコンテキストを制御するためにproxy関数を利用して叩く。

var App = Spine.Controller.sub({
    elements: {
        '#mod-regist input': 'input'
    },
    events: {
        'click button': 'create'
    },
    init: function(){
        Task.bind('create', this.proxy(this.add));
    },
    create: function(){
        if(!this.input.val()) return;
        var task = Task.create({
            task: this.input.val()
        });
    },
    add: function(){}
});

10. Tasksコントローラの生成

レコードを元にビューを作ってレンダリングするためにレコードを渡しつつTasksコントローラを作成する。 また、書き出し先をプロパティとして使用したいのでelementsに設定しておく。

var App = Spine.Controller.sub({
    elements: {
        '#mod-regist input': 'input',
        '#mod-tasks ul': 'lists'
    },
    events: {
        'click button': 'create'
    },
    init: function(){
        Task.bind('create', this.proxy(this.add));
    },
    create: function(){
        if(!this.input.val()) return;
        var task = Task.create({
            task: this.input.val()
        });
    },
    add: function(task){
        var view = new Tasks({
            item: task
        });
        this.lists.append(view.render().el);
    }
});

11. レンダリング

el要素はインスタンス化時に何も渡さなければ空のdiv要素が生成されるがここではliタグを生成して欲しいので、tagプロパティにliを設定。renderメソッドではインスタンス化時に設定したレコード(item)とテンプレートを利用してHTMLを生成、el要素に追加する。 また、メソッドチェーン用に自身をreturnしておく。

var Tasks = Spine.Controller.sub({
    tag: 'li',
    render: function(){
        this.el.html($('#mod-tmpl-list').tmpl(this.item));
        return this;
    }
});

12. テンプレート用のHTMLを用意する

テンプレートはjQuery.tmplを使用。わすれずに読み込んでおくこと。

<script type="text/x-jquery-tmpl" id="mod-tmpl-list">
    <span>${task}</span>
</script>

13. 訪問時の挙動1

とりあえず入力データを保存するところまでは実装できたけどこのままじゃ訪問時に何も表示されないので、Appコントローラ初期化時にモデルのクラスメソッドであるfetchメソッドを使用して訪問時にローカルストレージからデータを取得する処理を実装する。

var App = Spine.Controller.sub({
    elements: {
        '#mod-regist input': 'input',
        '#mod-tasks ul': 'lists'
    },
    events: {
        'click button': 'create'
    },
    init: function(){
        Task.bind('create', this.proxy(this.add));
        Task.fetch();
    },
    create: function(){
        if(!this.input.val()) return;
        var task = Task.create({
            task: this.input.val()
        });
    },
    add: function(task){
        var view = new Tasks({
            item: task
        });
        this.lists.append(view.render().el);
    }
});

14. 訪問時の挙動2

データを読み込んだだけでは何も処理されないので、fetch後に発行されるrefreshイベントを監視し、モデルのクラスメソッドであるeachメソッドを利用して全レコードを書き出す処理を実装する。 ちなみにモデル(Task)からメソッドを叩くのでコンテキストを制御するためにproxy関数を利用して叩く。

var App = Spine.Controller.sub({
    elements: {
        '#mod-regist input': 'input',
        '#mod-tasks ul': 'lists'
    },
    events: {
        'click button': 'create'
    },
    init: function(){
        Task.bind('create', this.proxy(this.add));
        Task.bind('refresh', this.proxy(this.addAll));
        Task.fetch();
    },
    create: function(){
        if(!this.input.val()) return;
        var task = Task.create({
            task: this.input.val()
        });
    },
    add: function(task){
        var view = new Tasks({
            item: task
        });
        this.lists.append(view.render().el);
    },
    addAll: function(){
        Task.each(this.proxy(this.add));
    }
});

ここまでの実装例は下記。

次回は今回のソースを元に削除ボタンを追加してみる。

Fork me on GitHub