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


はじめに

現在、個人的にVue.jsを勉強しております。
個人ブログの方に、経過はまとめております。
(まだ基本文法くらいですが。。。)
Vue.js入門その1〜基本文法〜
Vue.js入門その2〜Vueインスタンスってなんぞ?〜

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

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

準備

External ResourcesにてBootstrapを読み込み

CSSは面倒なので、Bootstrapを使用したいと思います。
CDNのURLを取得し、JS Fiddleの左側、External Resourcesに入力しておきます。

このとき、BootstrapのJSはJQueryを必要とするのでCSSのみにしておきます。
(後でコンソールでエラーが出てしまうので。。。)

目標のかたちをモック作成

<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インスタンスの作成

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

+ <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
イベントハンドリング
今回は後ほど、createTaskというメソッドを作りたいと思います。

    <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-modelnewTaskというプロパティをバインディングしています。
新規登録に際し、新しい値をどこかに保持する必要があると考えたのですが、とりあえず新しいインスタンスプロパティを用意し、そこに保持する方法としました。
(このあたりはもっとスマートな方法がありそうです。。。)
v-model

インスタンスに追記

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プロパティからの削除とします。

クリックイベントの設定

+  <div class="btn btn-danger" v-on:click="doneTask(task.id)">Done!</div>

メソッドの追記

  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の不整合が起こってしまうので、以下のように修正しました。

  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 追記
上記の修正に合わせ、tasksisDeletedをもたせて、タスクの完了を論理削除にしたいと思います。

Vueインスタンスの修正

先にVueインスタンスのcreateTaskメソッドとdoneTaskメソッドを修正していきます。

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

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

data: {
  tasks: [
    { id: 1, name: 'Studying JavaScript', isDeleted: false },
    { id: 2, name: 'Studying PHP',        isDeleted: false },
    { id: 3, name: 'Studying Ruby',       isDeleted: false },
  ],
}

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

  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
v-forとv-if
v-forv-ifを合わせて使用することで、条件分岐しつつループレンダリングできます。
今回はisDeletedがtrueのもののみ表示させます。

<!-- 省略 -->
<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入門その3〜簡単にTODOアプリを作ってみたよ〜