はじめてのVue.jsでToDoリスト作ってみた

#はじめに

本記事はVue.jsの初心者向けにToDoリストを用いてVue.jsの基本の書き方を解説するためのものです。

以下、ToDoリストの機能です。

・タスク追加機能
・タスク削除機能
・タスクの有無による画面の切り替え
・タスク数カウント機能
・リアルタイム検索機能

#動作

![Videotogif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/879778/3e3fc37f-23db-2612-a243-fc346757daf7.gif)

#成功コード

```ruby:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="app">
      <div>残タスク数：{{ remaining }}/{{ list.length }}</div>
      <input type="text" v-model="addText" placeholder="ToDoを入力して">
      <button v-on:click="addToDo()">追加</button>
      <button v-on:click="deleteBtn()">削除</button></br>
      <input type="text" placeholder="キーワードを入力して" v-model="keyword"><hr>
      <div v-if="keyword !== '' ">
        <ul  v-for="list in filteredLists">
          <li>
            <span v-bind:class="{ done: list.isChecked }">
              <input type="checkbox" v-model="list.isChecked">{{ list.text }}
            </span>
          </li>
        </ul>
      </div>
      <div v-else>
        <div v-if="list.length">
          <ul v-for="todo in list">
            <li>
              <span v-bind:class="{ done: todo.isChecked }">
                <input type="checkbox" v-model="todo.isChecked">{{ todo.text }}
              </span>
            </li>
          </ul>
        </div>
        <div v-else>
          現在タスクはありません
        </div>
      </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
    <script src="main.js"></script>
    
</body>
</html>
```

```ruby:main.js
var app = new Vue({
  el: '#app',
  data: {
      list: [],
      addText: '',
      keyword: ''
  },

  //watchでlistの変更を監視
  watch: {
      list: {
          handler: function() {
              //localStorageにデータを保存
              localStorage.setItem("list", JSON.stringify(this.list));
          },
          deep: true
      }
  },
  //マウントされた時にlocalStorageからデータを取得
  mounted: function() {
      this.list = JSON.parse(localStorage.getItem("list")) || [];
  },
  methods: {
      addToDo: function() {
          if (this.addText !== '') {
              this.list.push({
                  text: this.addText, 
                  isChecked: false,
              });
          }
          this.addText = '';
      },

      deleteBtn: function() {
          this.list = this.list.filter(function(todo) {
              return !todo.isChecked;
          });
      },

      filterLists: function () {
        var filtered = [];
        for (var i in this.list) {
            var list = this.list[i];
            var text = this.addText
            if (list.text.indexOf(this.keyword) !== -1) {
                filtered.push(list);
            }
         }
        return filtered;
      }
  },

  computed: {
    remaining: function() {
        var count = 0;
        var todos = this.list;
        var length = todos.length;
        for(var i = 0; i < length; i++) {
            if(!todos[i].isChecked) {
                count++;
            }
        }
        return count;
    },

    filteredLists: function () {
        return this.filterLists();
    }
    
}
});
```
#解説

それでは、1つずつ解説していきたいと思います。

###タスク入力・追加・削除機能
これはHTMLでいうと`inputタグ`と`buttonタグ`の部分に当たります。

```ruby:index.html
<input type="text" v-model="addText" placeholder="ToDoを入力して">
<button v-on:click="addToDo()">追加</button> 
<button v-on:click="deleteBtn()">削除</button>
```

まず、タスクの入力についてですが、`v-model`を利用して**Vue.js側でデータが更新されるよう**にします。
この設定を行うことで`buttonタグ`の、`v-onディレクティブ`に設定している各関数の処理を起動することができます。

次に追加・削除機能ですが、こちらは`v-onディレクティブ`でクリックされた時にイベント発火するようにしています。
そして、クリックイベントが起きた時、各機能で追加・削除するための関数を呼び出しています。

関数の中身は、このような感じです。

```ruby:main.js
addToDo: function() {
   if (this.addText !== '') {
     this.list.push({
      text: this.addText, 
      isChecked: false,
    });
  }
  this.addText = '';
},

deleteBtn: function() {
   this.list = this.list.filter(function(todo) {
      return !todo.isChecked;
   });
}
```

ちなみに、`data`の部分についは以下のように定義しました。

```ruby:main.js
data: {
      list: [],
      addText: '',
      keyword: ''
  },
```

まず、`addToDo`についてですが、これは条件分岐で、入力欄にテキストが入力された時に`list配列`にチェックボックスが空の値を格納するという動きをします。
そして、忘れがちなのが、最後の行の`this.addText = '';`です。

これは、タスクを追加したあとに入力欄に入力された文字がそのまま残っているという状態を防ぐ働きをします。

次に`deleteBtn`ですが、ここでのポイントは`filter()`です。
filterの基本的な記述は以下のように記述します。

```
var items = 配列データ;
 
items.filter( コールバック関数 )
```

そして、`filter`の気になる役割ですが、これは**特定の条件を与えて**配列データで取得したい内容を「コールバック関数」に書くことで、任意のデータを抽出して新しい配列を生み出すという役割があります。

つまり、ここではToDoリストのチェックボックスにチェックが入っているリストを抽出し、新しい`this.list`という配列に格納しているということです。

これらをHTML側で呼び出しイベント発火することで、タスクの入力から追加・削除までの機能を実装することができます。

###タスクの表示・タスクの有無による画面の切り替え

タスクの表示と画面の切り替えは、以下のコードです。

```ruby:index.html
<div v-if="list.length">
   <ul v-for="todo in list">
      <li>
        <span v-bind:class="{ done: todo.isChecked }">
          <input type="checkbox" v-model="todo.isChecked">{{ todo.text }}
        </span>
      </li>
   </ul>
</div>
<div v-else>
  現在タスクはありません
</div>
```

まずToDoリストが格納させている配列の一覧を表示させたいので`ulタグ`に`v-forディレクティブ`を指定して配列データを表示するよう指示してあげます。

次に`spanタグ`に`v-bindディレクティブ`を指定して、完了したタスクに**CSSのクラスを付与**するようにします。
クラス名は`done`で、**todoがチェックされている時**にCSSが適用されるように指定します。

つまり、チェックが入るとCSSにより打消し線が入るようにしているという仕様になっているわけです。

`inputタグ`ではチェックボックスを作成しています。こちらでは`v-modelディレクティブ`を使用し、**Vue.js側でチェック判定のデータのやり取りをする**ようにしています。

最後に`{{ todo.text }}`とすることで、Vue.jsからリストに格納されているtextを表示することができます。

そして、ToDoの有無による条件分岐として`v-if,v-elseディレクティブ`を使用し表示を切り替えます。

条件式としては`v-if="list.length`とし配列の中身があるかどうかを判定しています。

###タスクカウント機能

こちらは総数は、そのまま配列の合計数を表示するだけでいいので、`{{ list.length }}`としますが、残数はチェックされているかの計算が必要になるので、算出プロパティとして処理を値として出力するようにします。

```ruby:main.js
computed: {
    remaining: function() {
        var count = 0;
        var todos = this.list;
        var length = todos.length;
        for(var i = 0; i < length; i++) {
            if(!todos[i].isChecked) {
                count++;
            }
        }
        return count;
    }
```

まず、配列と配列の合計数を`todos`と`length`という変数に代入します。

そしてfor文を使用し、配列の合計が**0より大きい時**にcountを増加させるようにし、最後に戻り値としてカウントを返すようにします。

この`remaining`という値を、あとはHTML側で`{{ remaining }}として呼び出すだけでOKです。


###リアルタイム検索機能

いよいよ、最後の機能です。
もう少しですので、がんばりましょう！！

正直、僕はここに一番手こずりました。

以下、コードです。

```ruby:index.html
<input type="text" placeholder="キーワードを入力して" v-model="keyword"><hr>
<div v-if="keyword !== '' ">
  <ul  v-for="list in filteredLists">
    <li>
      <span v-bind:class="{ done: list.isChecked }">
        <input type="checkbox" v-model="list.isChecked">{{ list.text }}
      </span>
     </li>
   </ul>
</div>
<div v-else>
  <!-- 通常のToDoリストの表示 -->
</div>
```

```ruby:main.js

data: {
      list: [],
      addText: '',
      keyword: ''
  },

methods: {

      filterLists: function () {
        var filtered = [];
        for (var i in this.list) {
            var list = this.list[i];
            var text = this.addText
            if (list.text.indexOf(this.keyword) !== -1) {
                filtered.push(list);
            }
         }
        return filtered;
      }
  },

computed: {

    filteredLists: function () {
        return this.filterLists();
    }
    
  }
```

まず検索ワードの入力欄ですが、こちらはToDoの入力欄と同じように`v-model`で`data`の`keyword`を指定することで、**Vue.js側でデータ(keywordの値)の更新が行われる**ようにします。

そして`v-if,v-else`を使用し検索しているときと、そうでない時で表示を切り替えます。

ですので、条件式としては`v-if="keyword !== ''　"`とすることで、検索欄に入力がある時、検索の処理をするという分岐を作ることができます。

次に検索されたリストの表示ですが、配列の表示で使う`v-for`を利用します。

ここでは算出プロパティで定義している`filteredLists`の中の配列を表示したいので、`v-for="list in filteredLists"`と定義します。

肝心の`filteredLists`の内容ですが、戻り値として`filterLists()メソッド`を出力するようにしています。
つまり、この検索機能で最も重要となるのは、**filterLists()メソッド**ということです。

それでは、そのメソッドの内容を見ていきましょう。

```ruby:main.js
filterLists: function () {
        var filtered = [];
        for (var i in this.list) {
            var list = this.list[i];
            var text = this.addText;
            if (list.text.indexOf(this.keyword) !== -1) {
                filtered.push(list);
            }
         }
        return filtered;
      }
```

上記がそのメソッドの中身です。

まず、**通常表示のToDoリストの配列とは別に検索した時用の配列を定義する必要**があります。

そして、for-in文を使い、dataに定義されているlist配列の中身を取り出します。

そして、通常のToDoリストの中身と、ToDo入力欄の値を各変数に代入し、if文で検索にかけます。

今回は文字列の検索をしたいので`indexOf()`を使用します。
そして、検索したい内容の指定ですが、ToDoのテキストを検索にかけたいので、if文の上で定義した変数を使用して条件とします。

※なんで`!== -1`なのかはわかりませんでしたが、このような書き方をした記事を多数見たので、調べたい人は調べてください。
筆者は調べることを諦めました。。。

はい。ということで、条件に合致したときのことを解説します。

条件に合致すると、見ての通り合致した値を検索用に定義している配列に格納するようにしています。

そして、最後に戻り値として検索用の配列を指定してあげることで、検索欄に入力が合った時に、合致するものを表示することがデキるようになります。

ここで、**「記述を減らしたいなら、このメソッドだけにして算出プロパティでこのメソッドをわざわざ呼び出さなくても良いのでは？」**
と思う人もいますが、

今回の仕様ではリアルタイムでの検索なので**双方向データバインディング**にして**検索欄に入力が合ったときだけ**処理を実行したいので算出プロパティとして出力するようにしています。

この辺は難しいですが、メソッドだと関係ない箇所で画面に描画が入ると見た目では処理が動作しなくても裏で処理が動作しているので、無駄に読み込みが遅くなったりします。

しかし、算出プロパティなら、そのようなことが起きず、こちらが指定した場所に描画があったときだけ処理をリアルタイムで実行することができるので、それぞれの違いを理解し仕様に合わせた使い分けが必要です。

それでは、今回の解説は以上です。
Vue.jsは初心者の方には難しいとは思いますし、僕自身も初心者なので、こんな簡単なアプリでも結構、大変でした。

少しでも参考になれば幸いです。