Monaca + Onsen UI for Vueを触ってTodoアプリを作ってみた

今まではAngularの1.4とOnsen UIでMonacaアプリを作っていたのですが、新しくMonacaアプリを作る際のフレームワークとして[Onsen UI for Vue](https://ja.onsen.io/vue/)を使ってみたかったので、試しに簡単なTODOアプリを作ってみました。

# Monaca CLIでプロジェクト作成

Onsen UI for Vueはコードのコンパイルが必要なのでクラウドIDEでは作成できません。

コンパイルが必要なMonacaの新規プロジェクトの作成には、[MonacaCLI](https://docs.monaca.io/ja/monaca_cli/)が便利です！

MonacaCLIの詳しい紹介は省きますが、今回は `Onsen UI and Vue.js` のミニマムテンプレートをベースに作ってみます。

<img width="375" alt="001.png" src="https://qiita-image-store.s3.amazonaws.com/0/3531/7f4bf151-403d-9e00-4030-d9d88297ee92.png">

プロジェクトを作成して `monaca preview` でプレビューしてみるとこんな画面が表示されます。

### ミニマムテンプレートの中身を覗いてみる

<img width="600" alt="002.png" src="https://qiita-image-store.s3.amazonaws.com/0/3531/0c23ec99-c14e-a715-83a7-53f868400a56.png">

何やらたくさんファイルが入ってます。`www` フォルダがファイルのコンパイル先で最終的にアプリになる部分です。
`src` フォルダにコンパイル前のファイルが入っているので今回触るのはこっちです。

```main.js
import 'onsenui';
import Vue from 'vue';
import VueOnsen from 'vue-onsenui';

// Onsen UI Styling and Icons
require('onsenui/css-components-src/src/onsen-css-components.css');
require('onsenui/css/onsenui.css');

import App from './App.vue';

Vue.use(VueOnsen);

new Vue({
  el: '#app',
  template:'<app></app>',
  components: { App }
});
```

```html:App.vue
<template>
    <v-ons-page>
      <v-ons-toolbar>
        <div class="center">{{ title }}</div>
        <div class="right">
          <v-ons-toolbar-button>
            <v-ons-icon icon="ion-navicon, material: md-menu"></v-ons-icon>
          </v-ons-toolbar-button>
        </div>
      </v-ons-toolbar>
      <div style="text-align: center; padding-top:10px">Hello World!</div>
      <p style="text-align: center">
        <v-ons-button @click="alert">Click Me!</v-ons-button>
      </p>
    </v-ons-page>
</template>
<script>
  export default{
    data() {
      return {
        title: 'My app'
      };
    },
    methods: {
      alert() {
        this.$ons.notification.alert('This is an Onsen UI alert notification test.');
      }
    }
  };
</script>
```

main.jsの中でOnsen UIのモジュールのインポートやVueインスタンスの作成を行っていて、HTMLのテンプレート部分などはインポートしているApp.vueに書かれているので、今回はこちらのApp.vueファイルをさわってTodoアプリを作ってみようと思います。

今回はかなり簡易的なものなので、ルーティングや細かいコンポーネントごとにファイルを分けるなどはしないです。

# Todoリストの繰り返しを作成

[Onsen UIのドキュメント](https://ja.onsen.io/v2/api/vue/)を参考にしながら、`<template>` の中をこんな感じにしてみました。
dataには `todos` という配列を作って、`v-for`で繰り返し処理します。

```html:app.vue
<template>
  <v-ons-page>
    <v-ons-toolbar>
      <div class="center">{{ title }}</div>
    </v-ons-toolbar>

    <div class="listTodos">
      <v-ons-list-item v-for="(todo, $index) in todos" :class="{ isFinished: todo.finished }">
        <label class="left">
          <v-ons-checkbox
            :input-id="'checkbox-' + $index"
            :value="todo.text"
            v-model="todo.finished"
          >
          </v-ons-checkbox>
        </label>
        <label class="center" :for="'checkbox-' + $index">
          {{ todo.text }}
        </label>
      </v-ons-list-item>
    </div>

  </v-ons-page>
</template>
<script>
  export default{
    data() {
      return {
        title: 'Todo Sample',
        todos: [
          {
            'text': 'タスク名',
            'finished': false
          },
          {
            'text': '完了したタスク',
            'finished': true
          }
        ]
      };
    },
  };
</script>
```

<img width="375" alt="003.png" src="https://qiita-image-store.s3.amazonaws.com/0/3531/77bd5af1-a753-42e0-4f63-82bb5c592953.png">

表示されました！
ほぼHTMLで、OnsenUIのタグも直感的に書けて良い感じです。

# タスクを追加するフォームを作成
フォームを作って、新規タスクを追加できるようにします。

```HTML:
<div class="formNewTodo">
  <v-ons-input
    type="text"
    placeholder="新しいタスクを追加"
    v-model="newTodo"
  >
  </v-ons-input>
  <v-ons-button type="submit" @click="addNewTodo()">追加</v-ons-button>
</div>
```

`newTodo` というデータを作って、 入力エリアにバインディングします。

```JS:
data() {
  return {
    // 省略
    newTodo: '',
  };
},
methods: {
  addNewTodo () {
    // 入力されてない時は何もしない
    if (this.newTodo === '') return;

    var todo = {
      'text': this.newTodo,
      'finished': false
    }
    this.todos.push(todo);
    this.newTodo = '';
  },
},
```

submitしたら先程作った配列に追加されるようにしました。
それだけで画面に反映してくれるので便利ですね。

<img width="375" alt="004.png" src="https://qiita-image-store.s3.amazonaws.com/0/3531/8e39a4ef-2d5f-c0c5-1432-d2e9f6d89d17.png">

CSSは後で調整します。

# データをローカルストレージに保存する

これだけではデータの保存ができないので、ローカルストレージにデータを保存するようにします。

```JS:
var STORAGE_KEY = 'vue-todo';

export default{
  data() {
    return {
      title: 'Todo Sample',
      todos: JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'),
      newTodo: '',
    };
  },
  methods: {
    addNewTodo () {
      // 省略
    },
  },
  watch: {
    todos: {
      handler: function (todos) {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
      },
      deep: true
    }
  }
};
```

`todos` をウォッチして、変更があるたびにローカルストレージに保存してくれるようにしました。
`deep: true` の記載がないと、配列の要素が増減した時は反応してくれるのですが、配列の中身の値が変わった時に反応してくれません。少しハマりました。

![005.png](https://qiita-image-store.s3.amazonaws.com/0/3531/a0d81b00-3e37-2194-f025-392fe5c6e67e.png)

これで、タスクを追加した時や完了した時に、ローカルストレージにデータを保存してくれるようになりました。

# CSSで見た目を調整する

同じApp.vueの中に `<style>` ブロックを作成してCSSを書きます。
`scoped` の属性をつけると、このコンポーネントの中だけで適用されるスタイルになってくれるので管理しやすくなります。

```CSS:
<style scoped>
.formNewTodo {
  padding: 14px;
  display: flex;
  justify-content: space-between;
}
ons-input {
  flex: 1;
  border-bottom: 1px solid #ddd;
  margin-right: 14px;
}
.list-item {
  padding-right: 14px;
}
.list-item.isFinished {
  color: #ccc;
  text-decoration: line-through;
}
</style>
```

<img width="375" alt="006.png" src="https://qiita-image-store.s3.amazonaws.com/0/3531/edf93a3a-6f9c-4fd5-1b11-394d65b886da.png">

CSSが適用されました。

# コードまとめ

```HTML:App.vue
<template>
  <v-ons-page>
    <v-ons-toolbar>
      <div class="center">{{ title }}</div>
    </v-ons-toolbar>

    <div class="formNewTodo">
      <v-ons-input
        type="text"
        placeholder="新しいタスクを追加" float
        v-model="newTodo"
      >
      </v-ons-input>
      <v-ons-button type="submit" @click="addNewTodo()">追加</v-ons-button>
    </div>

    <div class="listTodos">
      <v-ons-list-item v-for="(todo, $index) in todos" :class="{ isFinished: todo.finished }">
        <label class="left">
          <v-ons-checkbox
            :input-id="'checkbox-' + $index"
            :value="todo.text"
            v-model="todo.finished"
          >
          </v-ons-checkbox>
        </label>
        <label class="center" :for="'checkbox-' + $index">
          {{ todo.text }}
        </label>
      </v-ons-list-item>
    </div>

  </v-ons-page>
</template>

<script>
var STORAGE_KEY = 'vue-todo';

export default{
  data() {
    return {
      title: 'Todo Sample',
      todos: JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'),
      newTodo: '',
    };
  },
  methods: {
    addNewTodo () {
      // 入力されてない時は何もしない
      if (this.newTodo === '') return;

      var todo = {
        'text': this.newTodo,
        'finished': false
      }

      this.todos.push(todo);
      this.newTodo = '';
    },
  },
  watch: {
    todos: {
      handler: function (todos) {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
      },
      deep: true
    }
  }
};
</script>

<style scoped>
.formNewTodo {
  padding: 14px;
  display: flex;
  justify-content: space-between;
}
ons-input {
  flex: 1;
  border-bottom: 1px solid #ddd;
  margin-right: 14px;
}
.list-item {
  padding-right: 14px;
}
.list-item.isFinished {
  color: #ccc;
  text-decoration: line-through;
}
</style>
```
# まとめ・所感

- テンプレート構文で、ほぼHTMLの中に属性やタグを書く形で使えるので学習コストが低い。OnsenUIのタグも直感的に使える。
- フォームの双方向バインディング便利。 テンプレート構文は、Angularの1系に 近い感じがするので慣れてる人は 使いやすそう。
- .Vueファイルの中に、HTML/CSS/JSが、 まとめて書けるのがわかりやすい。

Todoの削除や更新ができない簡易的なアプリですが、比較的簡単に作成することが出来ました。
ビルドまではしてませんが、MonacaCLIでコンパイルして、そのままビルドすればアプリとして使えるようになるはずです（未確認ですみません）

Vue.jsは学習コストが低めで使いやすいと思ったので、またアプリを作ってみようと思います。