Vue.jsの勉強で超簡単なタスク管理アプリを作ってみる

# はじめに
現在、個人的にVue.jsを勉強しております。
個人ブログの方に、経過はまとめております。
（まだ基本文法くらいですが。。。）
[Vue.js入門その１〜基本文法〜](https://re-engines.com/2017/06/23/vue-js%E5%85%A5%E9%96%80%E3%81%9D%E3%81%AE%EF%BC%91%E3%80%9C%E5%9F%BA%E6%9C%AC%E6%96%87%E6%B3%95%E3%80%9C/)
[Vue.js入門その２〜Vueインスタンスってなんぞ？〜](https://re-engines.com/2017/07/07/vue-js%E5%85%A5%E9%96%80%E3%81%9D%E3%81%AE%EF%BC%92%E3%80%9Cvue%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%80%9C/)

ただ、私がJSフレームワークの経験がないためだと思いますが、いまひとつピンとこなかったので、よくあるTODOアプリを作成してみました。
同様に勉強している方の参考になれば嬉しく思います。

サンプルはこちらです。（**7/12 リンクを差し替えました**）
[https://jsfiddle.net/naoki85/fo26rmr0/12/](https://jsfiddle.net/naoki85/fo26rmr0/12/)
JS Fiddleを使用しているので、Vue.jsのバージョンは **2.2.1** になります。

![スクリーンショット 2017-07-06 8.39.27.png](https://qiita-image-store.s3.amazonaws.com/0/118817/93f39081-4e1b-4d7b-e4de-91bf5869b93e.png)
# 準備
## External ResourcesにてBootstrapを読み込み
CSSは面倒なので、[Bootstrap](http://getbootstrap.com/getting-started/)を使用したいと思います。
CDNのURLを取得し、JS Fiddleの左側、External Resourcesに入力しておきます。

このとき、BootstrapのJSはJQueryを必要とするのでCSSのみにしておきます。
（後でコンソールでエラーが出てしまうので。。。）
## 目標のかたちをモック作成

```html
<table class="table table-striped">
  <thead>
    <tr>
      <th>Name</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Studying JavaScript</td>
      <td>
        <div class="btn btn-danger">Done!</div>
      </td>
    </tr>
    <tr>
      <td>Studying PHP</td>
      <td>
        <div class="btn btn-danger">Done!</div>
      </td>
    </tr>
    <tr>
      <td>Studying Ruby</td>
      <td>
        <div class="btn btn-danger">Done!</div>
      </td>
    </tr>
    <tr>
      <td><input class="form-control"></td>
      <td>
        <div class="btn btn-default">Create Task</div>
      </td>
    </tr>
  </tbody>
</table>
```
# タスクの一覧表示
## Vueインスタンスの作成

```js
var vm = new Vue({
  el: '#tasks-index',
  data: {
    tasks: [
      { id: 1, name: 'Studying JavaScript' },
      { id: 2, name: 'Studying PHP' },
      { id: 3, name: 'Studying Ruby' },
    ]
  }
})
```
## v-forを使用してループ処理
HTMLのテーブルタグの方に`id="tasks-index"`を割り当てておきます。
また、`v-for`を使用して`tasks`の値をループさせて出力します。
[v-for](https://jp.vuejs.org/v2/api/#v-for)

```diff
+ <table id="tasks-index" class="table table-striped">
  <thead>
    <tr>
      <th>Name</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
+    <tr v-for="task in tasks">
+      <td>{{ task.name }}</td>
      <td>
        <div class="btn btn-danger">Done!</div>
      </td>
    </tr>
    <tr>
      <td><input class="form-control"></td>
      <td>
        <div class="btn btn-default">Create Task</div>
      </td>
    </tr>
  </tbody>
</table>
```
これで、一覧表示はできましたので、次は新規登録です！
# タスクの新規登録
## クリックイベントの設定
「Create Task」ボタンが押されたら、`tasks`プロパティに値を追加したいので、クリックイベントを設定します。
`v-on:click`を使用すれば、設定したインスタンスメソッドにとばすことができます。
[v-on](https://jp.vuejs.org/v2/api/#v-on)
[イベントハンドリング](https://jp.vuejs.org/v2/guide/events.html)
今回は後ほど、`createTask`というメソッドを作りたいと思います。

```diff
    <tr>
+      <td><input v-model="newTask" class="form-control"></td>
      <td>
+        <div class="btn btn-default" v-on:click="createTask">Create Task</div>
      </td>
    </tr>
  </tbody>
</table>
```
また、入力フォームとして`input`タグを使用していますが、そこに`v-model`で`newTask`というプロパティをバインディングしています。
新規登録に際し、新しい値をどこかに保持する必要があると考えたのですが、とりあえず新しいインスタンスプロパティを用意し、そこに保持する方法としました。
（このあたりはもっとスマートな方法がありそうです。。。）
[v-model](https://jp.vuejs.org/v2/api/#v-model)
## インスタンスに追記

```diff
var vm = new Vue({
  el: '#tasks-index',
  data: {
    tasks: [
      { id: 1, name: 'Studying JavaScript' },
      { id: 2, name: 'Studying PHP' },
      { id: 3, name: 'Studying Ruby' },
    ],
+    newTask: '',
  },
+  methods: {
+    createTask: function (event) {
+      // 新しいIDを、配列長から取得
+      var new_id = this.tasks.length + 1;
+      // 配列に追加
+      this.tasks.push({ name: this.newTask });
+      // プロパティを空に戻す
+      this.newTask = '';
+    }
+  }
})
```

# タスクの完了（配列から削除）
タスクの完了は、要は`tasks`プロパティからの削除とします。
## クリックイベントの設定

```diff
+  <div class="btn btn-danger" v-on:click="doneTask(task.id)">Done!</div>
```
## メソッドの追記

```diff
  methods: {
    createTask: function () {
      var new_id = this.tasks.length + 1;
      this.tasks.push({ id: new_id, name: this.newTask });
      this.newTask = '';
+    },
+    doneTask: function (task_id) {
+      this.$delete(this.tasks, task_id - 1);
+    }
  }
```
# 新規登録時のid取得方法を修正
**7/7 追記**
riawiththesamさんにコメントにてご指摘いただきました。
JSON配列長から新IDを取得してしまうと、物理削除した後にIDの不整合が起こってしまうので、以下のように修正しました。

```diff
  methods: {
    createTask: function () {
-     var new_id = this.tasks.length + 1;
+     var new_id = this.tasks[this.tasks.length - 1].id + 1;
      this.tasks.push({ id: new_id, name: this.newTask });
      this.newTask = '';
   },
```
少しブサイクですが、配列の最後のidの値から計算するようにしました。
（もうちょっと良い書き方がありそうですが。。。）
# 論理削除に修正
**7/9 追記**
上記の修正に合わせ、`tasks`に`isDeleted`をもたせて、タスクの完了を論理削除にしたいと思います。
## Vueインスタンスの修正
先にVueインスタンスの`createTask`メソッドと`doneTask`メソッドを修正していきます。

```diff
methods: {
  createTask: function () {
    var new_id = this.tasks[this.tasks.length - 1].id + 1;
-   this.tasks.push({ id: new_id, name: this.newTask });
+   this.tasks.push({ id: new_id, name: this.newTask, isDeleted: false });
    this.newTask = '';
  },
  doneTask: function (task_id) {
-   this.$delete(this.tasks, task_id - 1);
+   this.tasks.filter(function (task) {
+     if (task.id === task_id) {
+       return task.isDeleted = true;
+     }
+   })
  }
}
```
`doneTask`でJSの`filter`関数を使用します。
これでidが一致した場合にその要素の`isDeleted`を書き換えます。
[filter](http://js.studio-kingdom.com/javascript/array/filter)

初期値にも`isDeleted`を追加しておきます。

```js
data: {
  tasks: [
    { id: 1, name: 'Studying JavaScript', isDeleted: false },
    { id: 2, name: 'Studying PHP',        isDeleted: false },
    { id: 3, name: 'Studying Ruby',       isDeleted: false },
  ],
}
```
* [forEach -MDN-](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach)
* [filter -MDN-](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/filter)

**7/12 追記**
私の理解不足でしたが、`filter`は新しい配列を作成してしまうので、`forEach`を使用した方が良い、というご指摘をいただきました。
そのため、`doneTask`メソッドを下記のように修正しました。

```diff
  doneTask: function (task_id) {
-   this.tasks.filter(function (task) {
+   this.tasks.forEach(function (task) {
      if (task.id === task_id) {
        return task.isDeleted = true;
      }
    })
  }
```
## v-forとv-ifで条件ループ
[v-if](https://jp.vuejs.org/v2/api/#v-if)
[v-forとv-if](https://jp.vuejs.org/v2/guide/list.html#v-for-と-v-if)
`v-for`と`v-if`を合わせて使用することで、条件分岐しつつループレンダリングできます。
今回は`isDeleted`がtrueのもののみ表示させます。

```diff
<!-- 省略 -->
<tbody>
-  <tr v-for="task in tasks">
+  <tr v-for="task in tasks" v-if="!task.isDeleted">
     <td>{{ task.name }}</td>
<!-- 省略 -->
```
# さいごに
簡単ではありますが、勉強用に作成してみました。
次の目標はRailsに組み込んで、もう少し手の込んだSPAを作りたいと思います！

もろもろミスがあり、申し訳ありません。

**7/10 追記**
本記事をブログの方でも公開しました。
よろしければご覧ください。
[Vue.js入門その３〜簡単にTODOアプリを作ってみたよ〜](https://re-engines.com/2017/07/10/vue-js%E5%85%A5%E9%96%80%E3%81%9D%E3%81%AE%EF%BC%93%E3%80%9C%E7%B0%A1%E5%8D%98%E3%81%ABtodo%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E4%BD%9C%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F%E3%82%88%E3%80%9C/)