Vueを使ってToDoリストを作ってみた

Vue.jsの公式チュートリアル、ずいぶん充実しているなぁ。
だけど、一通り終わっちゃったし、実務で使うまでにもういくつか練習してみたいな。

ということで、こちらのページで紹介されているチュートリアル「ToDoリストを作りながら学習しよう！」をベースに、ToDoリスト作ってみました。
https://cr-vue.mio3io.com/tutorials/todo.html

## 変更した点
### routerを使った
元サイトではindex.html、main.css、main.jsだけで実装していますが、せっかくの練習なので、vue-routerをつかいコンポーネントを外部から読み込んでくる形にしました（1ページだから意味ないけど）。以下構成です。
- App.vue
- main.js
- router 
　└ index.js
- views
　└ Top.vue

### ローカルストレージをそのまま使いました
ローカルストレージAPIから抜き出してくるのではなく、こちらに書いてある方法で直接ローカルストレージに読み書きしました。
https://jp.vuejs.org/v2/cookbook/client-side-storage.html

# 完成図
cssは上記サイトからコピペさせていただきましたm(__)m
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678689/a4417dee-4772-bafb-7f5f-c64b2048a45e.png)

# コード
``` App.vue
<template>
  <div id="app">
    <router-view />
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
</style>

```

``` main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router/'

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App),
}).$mount('#app')
```

``` router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const router = new VueRouter({
  mode : 'history',
  base : process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'top',
      component: () => import('../views/Top.vue')
    }
  ]
})

export default router
```

``` Top.vue
<template>
  <div class="top">
    <h2>TODOリスト</h2>

    <label v-for="(option, key, index) in options" :key="index">
      <input type="radio"
        v-model="current"
        v-bind:value="option.value" />{{ option.label }}
    </label>

    <span>{{ computedTodos.length }} 件を表示中</span>
    <table>
      <thead>
        <tr>
          <th class="id">ID</th>
          <th class="comment">コメント</th>
          <th class="state">状態</th>
          <th class="button">-</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in computedTodos" v-bind:key="item.id">
          <th>{{ item.id }}</th>
          <td>{{ item.comment }}</td>
          <td class="state">
            <button @click="doChangeState(item)">{{ labels[item.state] }} </button>
          </td>
          <td class="button">
            <button @click.ctrl="doRemove(item)">削除</button>
          </td>
        </tr>
      </tbody>
    </table>
    <p>※削除ボタンはコントロールキーを押しながらクリックしてください</p>

    <h2>新しい作業の追加</h2>
    <form @submit.prevent="doAdd" class="add-form">
      <p>
        <span>コメント</span>
        <input type="text" ref="comment">
        <button type="submit">追加</button>
      </p>
    </form>
  </div>
</template>

<script>
export default{
  name: 'top',
  data(){
    return {
      todos: [],
      options: [
        { value: -1, label: 'すべて' },
        { value:  0, label: '作業中' },
        { value:  1, label: '完了' }
      ],
      current: -1,
      uid: 0
    }
  },
  computed: {
    labels(){
      return this.options.reduce(function(a,b){
        return Object.assign(a, { [b.value]: b.label })
      }, {})
    },
    computedTodos: function(){
      return this.todos.filter(function(el) {
        return this.current < 0 ? true: this.current === el.state
      }, this)
    }
  },
  created(){
    if(localStorage.getItem('todos')){
      try{
        this.todos = JSON.parse(localStorage.getItem('todos'))
      } catch(e){
        localStorage.removeItem('todos')
      }
    }

    if(localStorage.getItem('uid')){
      this.uid = localStorage.getItem('uid')
    }
  },
  methods:{
    doAdd : function(){
      var comment = this.$refs.comment
      if(!comment.value.length){
        return
      }
      this.uid += 1
      this.todos.push({
        id: this.uid,
        comment: comment.value,
        state: 0
      })
      localStorage.setItem('uid',this.uid)
      comment.value = ''
    },
    doChangeState: function(item){
      item.state = item.state ? 0 : 1
    },
    doRemove: function(item){
      var index = this.todos.indexOf(item)
      this.todos.splice(index,1)
    }
  },
  watch: {
    todos: {
      handler: function(todos) {
        localStorage.setItem('todos',JSON.stringify(todos))
      },
      deep: true
    }
  }
}
</script>

<style>
省略 
</style>
```

## v-forでキー指定
``` Top.vue
    <label v-for="(option, key, index) in options" :key="index">
      <input type="radio"
        v-model="current"
        v-bind:value="option.value" />{{ option.label }}
    </label>
```
上記みたくキーを指定してあげないと下記みたく怒られたので、キーを指定しました。

```
5:5  error  Elements in iteration expect to have 'v-bind:key' directives  vue/require-v-for-key

✖ 1 problem (1 error, 0 warnings)
```

## clickを省略記法に変えました
``` Top.vue
          <td class="state">
            <button @click="doChangeState(item)">{{ labels[item.state] }} </button>
          </td>
          <td class="button">
            <button @click.ctrl="doRemove(item)">削除</button>
          </td>
```
Vue.jsでは"v-on:click"を"＠click"のように省略して書けるので省略しました。

## ローカルストレージへの読み書き
``` Top.vue
  created(){
    if(localStorage.getItem('todos')){
      try{
        this.todos = JSON.parse(localStorage.getItem('todos'))
      } catch(e){
        localStorage.removeItem('todos')
      }
    }
  }
```
こちらを参考にしてローカルストレージへの読み書きを直接する方法に変えました。
https://jp.vuejs.org/v2/cookbook/client-side-storage.html

``` Top.vue
    doAdd : function(){
      var comment = this.$refs.comment
      if(!comment.value.length){
        return
      }
      this.uid += 1
      this.todos.push({
        id: this.uid,
        comment: comment.value,
        state: 0
      })
      localStorage.setItem('uid',this.uid)
      comment.value = ''
    }
```
IDの更新は、uidという変数をローカルストレージとVueとで共有して更新していくやり方にしました。

``` Top.vue
  watch: {
    todos: {
      handler: function(todos) {
        localStorage.setItem('todos',JSON.stringify(todos))
      },
      deep: true
    }
  }
```
一瞬詰まったのが、ローカルストレージへのオブジェクトの保存。
ローカルストレージには配列やオブジェクトなど複雑なものは入れられないので、Stringに変換する必要があります。



シンプルで取り組みやすいチュートリアルをありがとうございました！
コンポーネント化とかvue-routerとかもっと触ってみたいので、次は複数ページにわたるアプリを作ってみたいと思います！