Force.comで動かすBackbone.jsのサンプル

Backbone.jsについて少し勉強したのでSalesforceのVisualforceで動かしてみました。

![2014-03-26_223123.png](https://qiita-image-store.s3.amazonaws.com/0/8407/1c3a6ec3-4ed9-5077-e03a-84a146bb9ba1.png)

Backbone.jsを使うときは下記のサイトから必要なファイルをダウンロードする必要があります。

**Backbonejs.org**
http://backbonejs.org/

このサイト内にGitHubへのリンクが用意されています。
そちらから必要なファイル一式をダウンロードできます。

**GitHub Repository**
https://github.com/jashkenas/backbone

今回必要なファイルは以下の3ファイルです。
- backbone.js
- jquery.js
- underscore.js

このファイルをZip形式で圧縮してSalesforceの静的リソースにアップロードします。

![2014-03-26_224708.png](https://qiita-image-store.s3.amazonaws.com/0/8407/0d0f2465-adb6-e4ab-d7fa-40808f48ea94.png)

これでBackbone.jsを動かすのに必要なファイルの準備が完了です。
次のコードでBackbone.jsをつかったサンプルページを確認できると思います。


まずベースとなるページです。

```html:BackBoneJsTaskApp.page
<apex:page title="Backbone.js" showHeader="true" sidebar="false">

    <apex:include pageName="BackBoneJsTaskAppCss" />
    
    <div id="vf-page">
    
        <h1>Tasks</h1>
        <form id="addTask">
            <input type="text" id="title" />
            <input type="submit" value="add" />
            <span id="error"></span>
        </form>
        
        <div id="tasks"></div>
        
        <p>Tasks Count: <span id='count'></span></p>
    
        <script type="text/template" id="task-template">
            <input type="checkbox" class="toggle" <%= completed ? 'checked': '' %>></input>
            <span class="<%= completed ? 'completed' : '' %>">
                <%- title %>
                <span class="delete">[x]</span>
            </span>
        </script>
    </div>
    
    <apex:includeScript value="{!URLFOR($Resource.BackBoneJS, 'js/underscore.js')}" />
    <apex:includeScript value="{!URLFOR($Resource.BackBoneJS, 'js/jquery.js')}" />
    <apex:includeScript value="{!URLFOR($Resource.BackBoneJS, 'js/backbone.js')}" />
    <apex:include pageName="BackBoneJsTaskAppScript"/>
</apex:page>
```


つぎにJavaScriptの処理を記載したページです。Backbone.jsの処理はこの中で行っています。

```js:BackBoneJsTaskAppScript.page
<apex:page >
    <script type="text/javascript">
        (function() {
        
            // Model
            var Task = Backbone.Model.extend({
                defaults: {
                    title: 'do something',
                    completed: false
                },
                validate: function(attrs) {
                    if ( _.isEmpty(attrs.title)) {
                        return 'title must not be empty';
                    }
                },
                initialize: function() {
                    this.on('invalid', function(model, error) {
                        $('#error').html(error);
                    })
                }
            });
            var Tasks = Backbone.Collection.extend({ model: Task});
        
            // View
            var TaskView = Backbone.View.extend({
                tagName: 'li',
                initialize: function() {
                    this.model.on('destroy', this.remove, this);
                    this.model.on('change', this.render, this);
                },
                events: {
                    'click .delete': 'destroy',
                    'click .toggle': 'toggle'
                },
                toggle: function() {
                    this.model.set('completed', !this.model.get('completed'));
                },
                destroy: function() {
                    if (confirm('are you sure?')) {
                        this.model.destroy();
                    }
                },
                remove: function() {
                    this.$el.remove();
                },
                template: _.template($('#task-template').html()),
                render: function() {
                    var template = this.template(this.model.toJSON());
                    this.$el.html(template);
                    return this;
                }
            });
            var TasksView = Backbone.View.extend({
                tagName: 'ul',
                initialize: function() {
                    this.collection.on('add', this.addNew, this);
                    this.collection.on('change', this.updateCount, this);
                    this.collection.on('destroy', this.updateCount, this);
                },
                addNew: function(task) {
                    var taskView = new TaskView({model: task});
                    this.$el.append(taskView.render().el);
                    $('#title').val('').focus();
                    this.updateCount();
                },
                updateCount: function() {
                    var uncompletedTasks = this.collection.filter(function(task) {
                        return !task.get('completed');
                    });
                    $('#count').html(uncompletedTasks.length);
                },
                render: function() {
                    this.collection.each(function(task) {
                        var taskView = new TaskView({model: task});
                        this.$el.append(taskView.render().el);
                    }, this);
                    this.updateCount();
                    return this;
                }
            });
        
            var AddTaskView = Backbone.View.extend({
                el : '#addTask',
                events: {
                    'submit': 'submit'
                },
                submit: function(e) {
                    e.preventDefault();
                    //var task = new Task({title: $('#title').val()});
                    var task = new Task();
                    if (task.set({title: $('#title').val()}, {validate: true})) {
                        this.collection.add(task);
                        $('#error').empty();
                    }
                }
            });
        
            var tasks = new Tasks([
                {
                    title: 'task1',
                    completed: true
                },
                {
                    title: 'task2'
                },
                {
                    title: 'task3'
                }
            ]);
        
            var tasksView = new TasksView({collection: tasks});
            var addTaskView = new AddTaskView({collection: tasks});
        
            $('#tasks').html(tasksView.render().el);
        })();
    </script>
</apex:page>
```

最後にCSSの処理を記載したページです。

```css:BackBoneJsTaskAppCss.page
<apex:page >
    <style type="text/css">
        #vf-page * {
            font-family: 'メイリオ', sans-serif;
            font-size: 18px;
        }
        
        #vf-page input[type=text] {
            font-size: 14px;
            padding: 5px;
            width: 200px;
        }
        
        #vf-page ul li {
            list-style-type: none;
        }
    
        #vf-page .completed {
            text-decoration: line-through;
            color: gray;
        }
        
        #error {
            color: red;
        }
    </style>
</apex:page>
```

CSSやJSの処理は外出しにした方がいいと思うのですが、静的リソースとして扱うと編集や管理がすこし面倒になると思います。そこでCSSやJSは別ページとして作成して、apex:inculdeで読み込む方法で試してみました。この方法でも問題なく動作してくれます。