Onsen UI＋Vue.js を使ってみた

# はじめに

HTML＋JavaScript でアプリ開発するにあたって、UI ライブラリをどうするか悩みました。モバイルアプリは Onsen UI がよさそうです。
Onsen UI は、当初 AngularJS をベースにしたフレームワークでしたが、2 になって別のフレームワークと一緒に使うのが容易になりました。
以前に Backbone.js＋Marionette.js と一緒に使ってみましたが、簡単なアプリでも記述量が少なくありません。そこで、Vue.js と一緒に使ってみたいと思います。

参考

* [Onsen UI＋Backbone.js＋Marionette.js を使ってみた - Qiita]
(http://qiita.com/tinymouse/items/7b1166c7a0f7c2aa3d29)
* [Vue.js を使ってみた - Qiita]
(http://qiita.com/tinymouse/items/d3eceea8d0b4237221f5)

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

* Onsen UI 2.0
* Vue.js 2.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 id="main-navi" page="list-page">
</ons-navigator>

<ons-template id="list-page">
    <ons-page id="list-page">
        <ons-toolbar id="list-toolbar">
            <div class="center">Try Onsen+Vue.js</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)

## Vue.js を使う

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

Vue.js
(https://jp.vuejs.org/)

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

```
lib
    vue.js
```

使用します。

```html:app.html
<script src="lib/vue.js"></script>
```

## データを準備する

まず、データを準備します。
Vue.js で扱うデータは通常の JavaScript オブジェクトです。ただし配列データを扱うときは、配列そのものでなく、配列データを含むオブジェクトを用意します。

```js:app.js
var app = {};
app.data = {
    contacts: [
        { 
            firstName: "Alice", lastName: "Arten", hasPhone: true, phoneNumber: "555-0184" 
        },{ 
            firstName: "Bob", lastName: "Brigham", hasPhone: true, phoneNumber: "555-0163" 
        },{ 
            firstName: "Charlie", lastName: "Campbell", hasPhone: true, phoneNumber: "555-0129" 
        }
    ]
};
```

## データをレンダリングする

```<ons-list id="content-list">``` にデータがリスト表示されるようテンプレートを追記します。
リストを表示するので ```v-for``` ディレクティブを使います。

```html:app.html
    <ons-list id="contact-list">
        <template v-for='item in list'>
            <ons-list-item>
                <div class="center">{{ item.firstName }} {{ item.lastName }}</div>
            </ons-list-item>
        </template>        
    </ons-list>
```

Vue のインスタンスを生成します。

```js:app.js
new Vue({
    el: '#contact-list',
    data: {
        list: app.data.contacts
    }
});
```

これをいつ実行すべきかですが、
el に指定する ```<ons-list id="contact-list">``` が生成されてからでないといけません。
これが含まれる ```<ons-page id="list-page">``` が生成されるタイミングで実行するにはこうします。↓

```js:app.js
$(document).on('init', '#list-page', function(){
    console.log("#list-page.init");
    new Vue({
        el: '#contact-list',
        data: {
            list: app.data.contacts
        }
    });
});
```

※ el に指定するのに Vue.js 1.0 では ```<ons-page id="list-page">``` でも問題なかったのですが、Vue.js 2.0 では問題あります。

この記述するために、jQuery を使います。

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

## ユーザ入力をハンドリングする

リストビューに「編集」と「削除」ボタンを追加します。
```v-on``` ディレクティブで実行したいメソッドを指定します。

```html:app.html
    <template v-for="item in list">
        <ons-list-item>
            中略
            <div class="right">
                <ons-button modifier="quiet" v-on:click='showDetail(item)'><ons-icon icon="pencil"></ons-icon></ons-button>
                <ons-button modifier="quiet" v-on:click='deleteItem(item)'><ons-icon icon="trash-o"></ons-icon></ons-button>
            </div>
```

実行するメソッドを設定します。

```js:app.js
new Vue({
    el: '#contact-list',
    中略
    methods: {
        showDetail: function(item){
            console.log("showDetail:", JSON.stringify(item));
        },
        deleteItem: function(item){
            console.log("deleteItem:", JSON.stringify(item));
        },
    }
});
```

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

```js:app.js
new Vue({
    中略
    methods: {
        deleteItem: function(item){
            中略
            var index = this.list.indexOf(item);
            this.list.splice(index, 1);
        }
```

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

※ Vue.js 1.0 では、配列から指定したデータを削除するのに、```$remove()``` メソッドを使いましたが、Vue.js 2.0 ではこれがなくなりました。

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

```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 placeholder="First Name"></ons-input>
            </ons-list-item>
            <ons-list-item>
                <ons-input placeholder="Last Name"></ons-input>
            </ons-list-item>
        </ons-list>
    </ons-page>
</ons-template>
```

```js:app.js
new Vue({
    中略
    methods: {
        showDetail: function(item){
            中略
            document.querySelector('#main-navi').pushPage('detail-page'); 
        },

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

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

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

```js:app.js
new Vue({
    中略
    methods: {
        showDetail: function(item){
            中略
            app.selected = item;
            document.querySelector('#main-navi').pushPage('detail-page'); 
        },
```

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

```html:app.html
<ons-template id="detail-page">
    中略
        <ons-list id="detail">
            <ons-list-item>
                <ons-input v-model='firstName' placeholder="First Name"></ons-input>
            </ons-list-item>
            <ons-list-item>
                <ons-input v-model='lastName' placeholder="Last Name"></ons-input>
            </ons-list-item>
```

データバインディングのため ```v-model``` ディレクティブを追記します。↑

```js:app.js
new Vue({
    el: '#detail',
    data: app.selected,
});
```

新たに Vue のインスタンスを生成します。data プロパティに app.selected をセットします。↑
これを実行するのもページ初期化のタイミングです。

```js:app.js
$(document).on('init', '#detail-page', function(){
    中略
    new Vue({
        el: '#detail',
        data: app.selected,
    });
});
```

※ el に指定するのに Vue.js 1.0 では ```<ons-page id="detail-page">``` でも問題なかったのですが、Vue.js 2.0 では問題あります。

これで app.selected の内容が画面に表示される・・はずですが、
そうなりません。
```v-model``` は ```<input>``` でないと使えないようです。
なので ```<ons-input>``` を ```<input>``` に書換します。

```html:app.html
<ons-template id="detail-page">
    中略
        <ons-list id="detail">
            <ons-list-item>
                <input v-model='firstName' class="text-input" placeholder="First Name">
            </ons-list-item>
            <ons-list-item>
                <input v-model='lastName' class="text-input" placeholder="Last Name">
            </ons-list-item>
```

## 双方向データバインディングする

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

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

さらに、リストビューの画面の内容も変わっています。

Vue.js のおかげで、双方向データバインディングが簡単にできます。

## カスタムディレクティブを使う

```v-model``` は ```<input>``` でないと使えないので ```<ons-input>``` を ```<input>``` に書換しましたが、不本意です。
Vue.js のカスタムディレクティブを使えばいいのではないかと、試してみました。

```js:app.js
Vue.directive('model-ex', {
    bind: function(el, binding, vnode){
        this.handler = function(){
            Vue.set(vnode.context, binding.expression, el.value);
        }.bind(this);
        el.addEventListener('input', this.handler);
        el.value = binding.value;
    },
    unbind: function(el){
        el.removeEventListener('input', this.handler);
    },
    update: function(el, binding){
        el.value = bindig.value;
    }
});
```

※ Vue.js 1.0 から 2.0 になったため書換しました。

```html:app.html
<ons-template id="detail-page">
      中略
            <ons-list-item>
                <ons-input v-model-ex='firstName' placeholder="First Name"></ons-input>
            </ons-list-item>
            <ons-list-item>
                <ons-input v-model-ex='lastName' placeholder="Last Name"></ons-input>
            </ons-list-item>
```

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

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

```html:app.html
<ons-template id="list-page">
    <ons-page id="list-page">
        <ons-toolbar id="list-toolbar">
            中略
            <div class="right">
                <ons-toolbar-button v-on:click='addItem'><ons-icon icon="fa-plus"></ons-icon></ons-toolbar-button>
            </div>
```

```js:app.js
new Vue({
    el: '#list-toolbar',
    methods: {
        addItem: function(){
            console.log("addItem:");
            var contact = {
                firstName: "", lastName: "", hasPhone: false, phoneNumber: "" 
            }; 
            app.data.contacts.push(contact);
            app.selected = contact;
            document.querySelector('#main-navi').pushPage('detail-page'); 
        }
```

※ el に指定するのに Vue.js 1.0 では ```<ons-page id="list-page">``` でも問題なかったのですが、Vue.js 2.0 では問題あります。

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

変更したり追加したりしたデータが、実行し直すとなくなってしまいます。
localStorage を使って保存できるようにします。

* JavaScriptプログラミング講座【ウェブストレージについて（Web Storage）】
(http://hakuhin.jp/js/storage.html)

起動時に読込します↓

```js:app.js
    app.data = JSON.parse(localStorage.getItem('data')) || app.data;
```

いつ保存すればいいでしょうか。

```js:app.js
new Vue({
    el: '#list-page',
    中略
    methods: {
        deleteItem: function(item){
            中略
            localStorage.setItem('data', JSON.stringify(app.data));
        },
```

データを削除したとき↑

```js:app.js
$(document).on('destroy', '#detail-page', function(){
    console.log("#detail-page.destroy");
    localStorage.setItem('data', JSON.stringify(app.data));
})
```

詳細画面から一覧画面に戻るとき↑