Onsen UI＋Backbone.js＋Marionette.js を使ってみた

# はじめに

HTML＋JavaScript でアプリ開発するにあたって、UI ライブラリをどうするか悩みました。モバイルアプリは Onsen UI がよさそうです。
Onsen UI は、当初 AngularJS をベースにしたフレームワークでしたが、2 になって別のフレームワークと一緒に使うのが容易になりました。
個人的に AngularJS より、Backbone.js＋Marionette.js の方が好みです。Onsen UI も Backbone.js＋Marionette.js と一緒に使ってみたいと思います。

# Onsen UI＋Backbone.js＋Marionette.js を使ってみた

* Onsen UI 2.0
* Backbone.js 1.1.2
* Marionette.js 2.4.1
* backbone.stikit 0.8.0
* backbone.localStorage 1.1.0

## Onsen UI を使う

以下のページからダウンロードします。

Onsen UI 2 
(https://onsen.io/v2/)

以下のファイルをワークスペースにコピーします。

```
lib
    onsen
        css
            onsenui.css
            onsen-css-components.css
        js
            onsenui.js
```

使用します。

```html:app.html
<link href="lib/onsen/css/onsenui.css" rel="stylesheet" />
<link href="lib/onsen/css/onsen-css-components.css" rel="stylesheet" />    
<script src="lib/onsen/js/onsenui.js" type="text/javascript"></script>
```

Onsen UI は独自タグを使います。

```html:app.html
<ons-navigator var="naviMain" page="list-page">
</ons-navigator>

<ons-template id="list-page">
    <ons-page id="list-page">
        <ons-toolbar>
            <div class="center">Try Onsen+Marionette</div>
        </ons-toolbar>
        <ons-list id="contact-list">
        </ons-list>
    </ons-page>
</ons-template>
```

ツールバーがついたリストビューが本体のページを準備します。

JavaScript のコードは ```ons.ready()``` 関数に記述します。

```js:app.js
ons.ready(function(){
    ....
});
```

ドキュメント

* Onsen UI 2 Docs - Onsen UI Framework (https://onsen.io/v2/docs/js.html)

## Backbone.js を使う

以下のページからダウンロードします。

Backbone.js
(http://backbonejs.org/)

以下のファイルをワークスペースにコピーします。

```
lib
    jquery.js
    json2.js
    underscore.js
    backbone.js
```

使用します。

```html:app.html
<script src="lib/jquery.js"></script>
<script src="lib/json2.js"></script>
<script src="lib/underscore.js"></script>
<script src="lib/backbone.js"></script>
```

## Backbone.Model と Backbone.Collection

* 試して学ぶ Backbone.js 入門 (https://appkitbox.com/knowledge/javascript/20130315-45)
* Backbone.js に入門してみる (http://yutapon.hatenablog.com/entry/2013/06/08/192521) 
* Backbone.js に入門してみる (http://yutapon.hatenablog.com/entry/2013/06/15/235250)

以上を参考にしてデータを準備します。

```js:app.js
var app = {};
app.Contact = Backbone.Model.extend({
    defaults: {
        firstName: "",
        lastName: "",
        phoneNumber: ""
    }
});

app.ContactList = Backbone.Collection.extend({
    model: app.Contact,
});
app.contacts = new app.ContactList([
    { 
        firstName: "Alice", lastName: "Arten", phoneNumber: "555-0184" 
    },{ 
        firstName: "Bob", lastName: "Brigham", phoneNumber: "555-0163" 
    },{ 
        firstName: "Charlie", lastName: "Campbell", phoneNumber: "555-0129" 
    }
]);
```

## Backbone.View

* 試して学ぶ Backbone.js 入門 (https://appkitbox.com/knowledge/javascript/20130906-58)
* Backbone.js に入門してみる (http://yutapon.hatenablog.com/entry/2013/06/23/213737)
* Backbone.js に入門してみる (http://yutapon.hatenablog.com/entry/2013/06/30/145750)

これはわかりづらいので Marionette.js を使います。

* そこそこコード量が増えてきたらMarionette.jsを併用するといいらしい (http://hozunomiya.xyz/?p=88)

## Marionette.js を使う

以下のページからダウンロードします。

Marionette.js 
(http://marionettejs.com/)

以下のファイルをワークスペースにコピーします。

```
lib
    ↑以上が Backbone.js に必須
    backbone.marionette.js
```

使用します。

```html:app.html
↑以上が Backbone.js に必須
<script src="lib/backbone.marionette.js"></script>
```

## Marionette.ItemView と Marionette.CollectionView

* "Backbone.Marionette.js: A Gentle Introduction" を今更ながら勉強してみた (http://qiita.com/mmyoji/items/7d00e6620c5a171d80b4)
* Marionette.js まとめ (http://qiita.com/tohashi/items/85475b2dbbea164179e5) 
* Marionette.js まとめ (http://qiita.com/tohashi/items/b5d1bac3f32d324527ce)

以上を参考にして

```html:app.html
<script type="text/template" id="contact-item-template">
    <ons-list-item class="contact-item">
        <div class="center"><%= firstName %> <%= lastName %></div>
    </ons-list-item>
</script>        
```
```js:app.js
app.ContactItemView = Marionette.ItemView.extend({
    template: '#contact-item-template',
});
app.ContactListView = Marionette.CompositeView.extend({
    el: '#list-page',
    template: '#list-page',
    childViewContainer: '#contact-list',
    childView: app.ContactItemView,
});

app.listView = new app.ContactListView({
    collection: app.contacts
});
app.listView.render();
```

## Marionette.View のイベント

リストビューに「編集」と「削除」ボタンを追加します。

```html:app.html
<script type="text/template" id="contact-item-template">
    <ons-list-item class="contact-item">
        中略
        <div class="center">
            <ons-button modifier="quiet" class="js-detail"><ons-icon icon="pencil"></ons-icon></ons-button>
            <ons-button modifier="quiet" class="js-delete"><ons-icon icon="trash-o"></ons-icon></ons-button>
        </div>
```

それぞれイベントハンドラを設定します。

```js:app.js
app.ContactItemView = Marionette.ItemView.extend({
    中略
    events: {
        'click .js-delete': 'onDeleteClick',
        'click .js-detail': 'onDetailClick'
    },
    onDeleteClick: function(e){
        e.preventDefault();
        e.stopPropagation();
        console.log("ContactItemView.onDeleteClick");
    },
    onDetailClick: function(e){
        e.preventDefault();
        e.stopPropagation();
        console.log("ContactItemView.onDetailClick");
    },
```

## データを削除できるようにする


```js:app.js
app.ContactItemView = Marionette.ItemView.extend({
    中略
    onDeleteClick: function(e){
        中略
        this.model.destroy();
    },
```

model を操作しただけだが、画面も変わります。

## 詳細ページを表示する


```html:app.html
<ons-template id="detail-page">
    <ons-page id="detail-page">
        <ons-toolbar>
            <div class="left"><ons-back-button>Back</ons-back-button></div>
            <div class="center">Detail</div>
        </ons-toolbar>
        <ons-list id="detail">
            <ons-list-item>
                <ons-input input-id="firstName" placeholder="First Name"></ons-input> 
            </ons-list-item>
            <ons-list-item>
                <ons-input input-id="lastName" placeholder="Last Name"></ons-input>
            </ons-list-item>
        </ons-list>
    </ons-page>
</ons-template>
```

```js:app.js
app.ContactItemView = Marionette.ItemView.extend({
    中略
    onDetailClick: function(e){
        中略
        naviMain.pushPage('detail-page');
    },

$(document).on('init', '#detail-page', function(){
    console.log("#detail-page.pageinit");
});
```

Onsen UI の機能でページ遷移します。

## 詳細ページに内容を表示する


```js:app.js
app.ContactItemView = Marionette.ItemView.extend({
    中略
    onDetailClick: function(e){
        中略
        app.selected = this.model;
        naviMain.pushPage('detail-page');
```

app オブジェクトに selected を追加して別のページに渡せるようにします↑

```js:app.js
app.ContactDetailView = Marionette.ItemView.extend({
    el: '#detail-page',
    ui: {
        firstName: '#firstName',
        lastName: '#lastName'
    },
    template: false,
    render: function(){
        $(this.ui.firstName).val(this.model.attributes.firstName);
        $(this.ui.lastName).val(this.model.attributes.lastName);
        return this;
    }
});

app.detailView = new app.ContactDetailView({
    model: app.selected
});
app.detailView.render();
```

ContactDetailView の model プロパティに app.selected をセットします↑

これを実行すると、new app.ContactDetailView したときエラーになります。
el で指定した ```<ons-page id="detail-page">``` が、この時点で生成されていないからです。
Onsen UI では pushPage() されてから DOM 生成されます。したがって、

```js:app.js
$(document).on('init', '#detail-page', function(){
    中略
    app.detailView = new app.ContactDetailView({
        model: app.selected
    });
    app.detailView.render();
});
```

## Backbone.stickit を使う

詳細ページは簡単にデータバインディングしたいと思います。

* backbone.stickit を使って Backbone でデータバインディング (http://tnakamura.hatenablog.com/entry/2013/12/16/120956)

以下のページからダウンロードします。

Backbone.stickit 
(https://github.com/NYTimes/backbone.stickit)

以下のファイルをワークスペースにコピーします。

```
lib
    ↑以上が Backbone.js に必須
    backbone.stickit.js
```

使用します。

```html:app.html
↑以上が Backbone.js に必須
<script src="lib/backbone.stickit.js"></script>
```

上記を参考にして

```js:app.js
app.ContactDetailView = Marionette.ItemView.extend({
    中略
    bindings: {
        '#firstName': 'firstName',
        '#lastName': 'lastName'
    },
    render: function(){
        /*
        $(this.ui.firstName).val(this.model.attributes.firstName);
        $(this.ui.lastName).val(this.model.attributes.lastName);
        ↓入替 */
        this.stickit();
```

model の内容が画面に表示されるだけでなく、
画面の要素の内容で model の内容が更新されるようになります。

app.selected はポインタです。値がコピーされているわけではありません。
detailView.model もポインタです。詳細画面の要素の内容を変えると 
ContactItemView.model の内容も変わっています。

でもリストビューの画面の内容は変わっていません。

## model の内容の変化に合わせて view を更新する

```js:app.js
app.ContactItemView = Marionette.ItemView.extend({
    中略
    modelEvents: {
        'change': 'onChange'
    },
    onChange: function(){
        console.log("ContactItemView.onChange");
        this.render();
    },
```

## データを追加できるようにする

ツールバーに「＋」ボタンを追加して、データを追加できるようにします。

```html:app.html
<ons-template id="list-page">
    <ons-page id="list-page">
        <ons-toolbar>
            中略
            <div class="right">
                <ons-toolbar-button class="js-add"><ons-icon icon="fa-plus"></ons-icon></ons-toolbar-button>
            </div>
```

```js:app.js
app.ContactListView = Marionette.CompositeView.extend({
    中略
    events: {
        'click .js-add': 'onAddClick',
    },
    onAddClick: function(e){
        var contact = new app.Contact();
        this.collection.add(contact);
        app.selected = contact;
        navMain.pushPage('detail-page');
    }
```

## データが保存されるようにする

変更したり追加したりしたデータが、実行し直すとなくなってしまいます。

* Backbone.js に入門してみる (http://yutapon.hatenablog.com/entry/2013/10/14/202217)
* LocalStorageを使ってデータを永続化する (https://github.com/hokaccha/backbone-hands-on/wiki/20:-LocalStorage%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E6%B0%B8%E7%B6%9A%E5%8C%96%E3%81%99%E3%82%8B)

以下のページからダウンロードします。

Backbone.localStorate 
(https://github.com/jeromegn/Backbone.localStorage)

以下のファイルをワークスペースにコピーします。

```
lib
    ↑以上が Backbone.js に必須
    backbone.localStorage.js
```

使用します。

```html:app.html
↑以上が Backbone.js に必須
<script src="lib/backbone.localStorage.js"></script>
```

事前設定↓

```js:app.js
app.ContactList = Backbone.Collection.extend({
    中略
    localStorage: new Store('ContactList.TryMarionetteJs')
```

起動時に読込します↓

```js:app.js
app.contacts.fetch();
app.listView = new app.ContactListView({
```

追加時に保存します↓

```js:app.js
app.ContactListView = Marionette.CompositeView.extend({
    中略
    onAddClick: function(e){
        var contact = new app.Contact();
        /*
        this.collection.add(contact);
        ↓入替*/
        this.collection.create(contact);
```

削除時に保存します。これで削除されたことが保存されます↓

```js:app.js
app.ContactItemView = Marionette.ItemView.extend({
    中略
    onDeleteClick: function(e){
        中略
        this.model.destroy();
```

画面で変更したのを保存するのはどこにセットするのがいいでしょうか↓

```js:app.js
app.ContactDetailView = Marionette.ItemView.extend({
    中略
    modelEvents: {
        'change': 'onChange'
    },
    onChange: function(){
        console.log("ContactDetailView.onChange");
        this.model.save();
    },
```

こんな手もあります↓

```js:app.js
app.ContactList = Backbone.Collection.extend({
    中略
    initialize: function(){
        this.on('change', this.onChange);
    },
    onChange: function(model){
        console.log("ContactList.onChange")
        model.save();
    }
```