VueJS チュートリアルをやってみた！

# VueJS チュートリアルをやってみた！

## 概要
VueJSの「[チュートリアル ToDoリストを作りながら学習しよう！](https://cr-vue.mio3io.com/tutorials/todo.html)」を今更ながらやってみました。(笑)
多少アレンジを加えています。


## 成果物
https://github.com/Akira-Demizu/vue-tutorials

## 環境
環境は `docker + vue-cli`を利用しました。
以下を参考に構築してみてください。

- [docker-composeでシンプルにvue.js環境構築](https://qiita.com/A-Kira/items/ed12de84dda0230a4eae)

## 構成

```
project
├── docker
│   └── web
│       └── Dockerfile
├── docker-compose.yml
└── server
    └── src
```

## 手順
### 1. 各種設定
今回はvue-cliを利用していますので、本家のチュートリアルとは構成が異なります。

#### CSSの用意
本家のチュートリアルでも`CSS の説明はしていないため、コピペして使用してください。`とありますように、本記事でもCSSの説明については触れませんので、[本家のCSS](https://github.com/mio3io/cr-vue/blob/master/codes/tutorial-todo/main.css)をコピペして利用します。

`main.css`を作成します。

```css:server/src/assets/css/main.css
* {
  box-sizing: border-box;
}
#app {
  max-width: 640px;
  margin: 0 auto;
}
table {
  width: 100%;
  border-collapse: collapse;
}
thead th {
  border-bottom: 2px solid #0099e4; /*#d31c4a */
  color: #0099e4;
}
th,
th {
  padding: 0 8px;
  line-height: 40px;
}
thead th.id {
  width: 50px;
}
thead th.state {
  width: 100px;
}
thead th.button {
  width: 60px;
}
tbody td.button, tbody td.state {
  text-align: center;
}
tbody tr td,
tbody tr th {
  border-bottom: 1px solid #ccc;
  transition: all 0.4s;
}
tbody tr.done td,
tbody tr.done th {
  background: #f8f8f8;
  color: #bbb;
}
tbody tr:hover td,
tbody tr:hover th {
  background: #f4fbff;
}
button {
  border: none;
  border-radius: 20px;
  line-height: 24px;
  padding: 0 8px;
  background: #0099e4;
  color: #fff;
  cursor: pointer;
}
```

#### App.vueの編集
- デフォルトロゴ画像削除
- `main.css`を読むように設定

```vue:server/src/App.vue
<template>
  <div id="app">
    <router-view/>
  </div>
</template>

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

<style>
@import "./assets/css/main.css";

#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

```

#### コンポーネントの作成
`server/src/components/Tutorial.vue`を作成しましょう。

以下のような`Tutorial.vue`を元に進めていきます。

```vue:Tutorial.vue
<template>
  <p>test</p>
</template>

<script>
export default {
  name: 'Tutorial',
  data () {}
}
</script>

<style scoped>
</style>
```

#### ルーティング設定

デフォルトの`HelloWold`から`Tutorial`コンポーネントに変更します。

```js:server/src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Tutorial from '@/components/Tutorial'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Tutorial',
      component: Tutorial
    }
  ]
})
```

### 2. リスト用のテーブル作成

```vue
<template>
  <div>
    <table>
      <!-- テーブルヘッダー -->
      <thead>
      <tr>
        <th class="id">ID</th>
        <th class="comment">コメント</th>
        <th class="state">状態</th>
        <th class="button">-</th>
      </tr>
      </thead>
      <tbody>
      <!-- [1] ここに <tr> で ToDo の要素を1行づつ繰り返し表示したい -->
      </tbody>
    </table>
  </div>
</template>
```

### 3. リストレンダリング
ToDo リストデータ用の空の配列を data オプションへ登録します。

```vue:server/src/components/Tutorial.vue
<template>
<!-- ... -->
</template>

<script>
export default {
  name: 'Tutorial',
  data () {
    return {
      todos: []
    }
  }
}
</script>
```

配列要素(`todos`)の数だけ繰り返し表示させるには、対象となるタグ（ ここでは <tr> タグ ）に v-for ディレクティブを使用します。

```vue:server/src/components/Tutorial.vue
<template>
  <div>
    <table>
      <!-- テーブルヘッダー -->
      <thead>
      <tr>
        <th class="id">ID</th>
        <th class="comment">コメント</th>
        <th class="state">状態</th>
        <th class="button">-</th>
      </tr>
      </thead>
      <tbody>
      <!-- ここに <tr> で ToDo の要素を1行づつ繰り返し表示 -->
      <tr v-for="item in todos" v-bind:key="item.id">
        <!-- 要素の情報 -->
      </tr>
      </tbody>
    </table>
  </div>
</template>
```

v-for を記述したタグとその内側で todos データの各要素のプロパティが使用できるようになります。 <tr> タグの内側に「ID」「コメント」「状態変更ボタン」「削除ボタン」のカラムを追加していきましょう。

```vue:server/src/components/Tutorial.vue
<template>
  <div>
    <table>
      <!-- テーブルヘッダー -->
      <thead>
      <tr>
        <th class="id">ID</th>
        <th class="comment">コメント</th>
        <th class="state">状態</th>
        <th class="button">-</th>
      </tr>
      </thead>
      <tbody>
      <!-- ここに <tr> で ToDo の要素を1行づつ繰り返し表示 -->
      <tr v-for="item in todos" v-bind:key="item.id">
        <th>{{ item.id }}</th>
        <td>{{ item.comment }}</td>
        <td class="state">
          <!-- 状態変更ボタンのモック -->
          <button>{{ item.state }}</button>
        </td>
        <td class="button">
          <!-- 削除ボタンのモック -->
          <button>削除</button>
        </td>
      </tr>
      </tbody>
    </table>
  </div>
</template>
```

### 4. フォームの入力値の取得

- テーブルの下に追加ボタンを追加
- `data`に`uid`を追加
- `doAdd`メソッドの追加

```vue:server/src/components/Tutorial.vue
<template>
  <div>
    <table>
      <!-- テーブルヘッダー -->
      <thead>
      <tr>
        <th class="id">ID</th>
        <th class="comment">コメント</th>
        <th class="state">状態</th>
        <th class="button">-</th>
      </tr>
      </thead>
      <tbody>
      <!-- ここに <tr> で ToDo の要素を1行づつ繰り返し表示 -->
      <tr v-for="item in todos" v-bind:key="item.id">
        <th>{{ item.id }}</th>
        <td>{{ item.comment }}</td>
        <td class="state">
          <!-- 状態変更ボタンのモック -->
          <button>{{ item.state }}</button>
        </td>
        <td class="button">
          <!-- 削除ボタンのモック -->
          <button>削除</button>
        </td>
      </tr>
      </tbody>
    </table>

    <h2>新しい作業の追加</h2>
    <form class="add-form" v-on:submit.prevent="doAdd">
      <!-- コメント入力フォーム -->
      コメント <input type="text" ref="comment">
      <!-- 追加ボタンのモック -->
      <button type="submit">追加</button>
    </form>
  </div>
</template>

<script>
export default {
  name: 'Tutorial',
  data () {
    return {
      todos: [],
      uid: 0
    }
  },
  methods: {
    // ToDo 追加の処理
    doAdd: function (event, value) {
      // ref で名前を付けておいた要素を参照
      var comment = this.$refs.comment
      // 入力がなければ何もしないで return
      if (!comment.value.length) {
        return
      }
      // { 新しいID, コメント, 作業状態 }
      // というオブジェクトを現在の todos リストへ push
      // 作業状態「state」はデフォルト「作業中=0」で作成
      this.todos.push({
        id: this.uid++,
        comment: comment.value,
        state: 0
      })
      // フォーム要素を空にする
      comment.value = ''
    }
  }
}
</script>
```

### 5. ストレージへの保存及び取得の自動化
todos データの内容が変わると、自動的にローカルストレージへ保存するようにしてみます。また、画面リロード時にはローカルストレージからデータを取得するようにします。

#### ローカルストレージを操作するプラグインの作成
本家ではとりあえず`main.js`に追加していますが、使いまわしを想定して今回はプラグインとして作成してみたいと思います。
まず、srcに`plugins`ディレクトリを作成します。その中に`todoStorage.js`を作成してみましょう。

```js:server/src/plugins/todoStorage.js
// https://jp.vuejs.org/v2/examples/todomvc.html
const STORAGE_KEY = 'todos-vuejs-demo'
const todoStorage = {
  fetch: function () {
    return JSON.parse(
      localStorage.getItem(STORAGE_KEY) || '[]'
    )
  },
  save: function (todos) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
  }
}
export default todoStorage
```

`todoStorage`をimportして利用します。
「インスタンス作成時」には`created`メソッドからデータを取得し、「データの変更時」には`watch`メソッドでデータを保存するようにします。

```js:server/src/components/Tutorial.vue
import todoStorage from '../plugins/todoStorage'

export default {
  // ...
  created () {
    // インスタンス作成時に自動的に fetch() する
    this.todos = todoStorage.fetch()
    this.uid = this.todos.length
  },
  watch: {
    // オプションを使う場合はオブジェクト形式にする
    todos: {
      // 引数はウォッチしているプロパティの変更後の値
      handler: function (todos) {
        todoStorage.save(todos)
      },
      // deep オプションでネストしているデータも監視できる
      deep: true
    }
  },
  // methods: {
  // ...
}
```

### 6. 状態の変更と削除の処理
メソッドの作成

```js:server/src/components/Tutorial.vue
import todoStorage from '../plugins/todoStorage'

export default {
  // ...
  methods: {
    // ToDo 追加の処理
    doAdd: function (event, value) {
      // ...
    },
    // 状態変更の処理
    doChangeState: function (item) {
      item.state = item.state ? 0 : 1
    },
    // 削除の処理
    doRemove: function (item) {
      const index = this.todos.indexOf(item)
      this.todos.splice(index, 1)
    }
  }
}
```

ボタンのモックを書き換えます。

```html:server/src/components/Tutorial.vue
<template>
  <div>
    <table>
      <tbody>
      <!-- ここに <tr> で ToDo の要素を1行づつ繰り返し表示 -->
      <tr v-for="item in todos" v-bind:key="item.id">
        <th>{{ item.id }}</th>
        <td>{{ item.comment }}</td>
        <td class="state">
          <button v-on:click="doChangeState(item)">
            {{ item.state }}
          </button>
        </td>
        <td class="button">
          <button v-on:click="doRemove(item)">
            削除
          </button>
        </td>
      </tr>
      </tbody>
    </table>
    ...
  </div>
</template>
```

### 7. リストの絞り込み機能
特定の作業状態のリストのみを表示させる「絞り込み機能」を追加します。

#### 選択用フォームの作成
選択肢の options リストを追加します。

```js:server/src/components/Tutorial.vue
export default {
  name: 'Tutorial',
  data () {
    return {
      todos: [],
      uid: 0,
      options: [
        { value: -1, label: 'すべて' },
        { value: 0, label: '作業中' },
        { value: 1, label: '完了' }
      ],
      // 選択している options の value を記憶するためのデータ
      // 初期値を「-1」つまり「すべて」にする
      current: -1
    }
  },
  // ...
}
```

#### 絞り込み機能実装
computed オプションに加工したデータを返すメソッドを登録します。 

```js:server/src/components/Tutorial.vue

export default {
  name: 'Tutorial',
  data () {
    // ...
  },
  computed: {
    computedTodos: function () {
      // データ current が -1 ならすべて
      // それ以外なら current と state が一致するものだけに絞り込む
      return this.todos.filter(function (el) {
        return this.current < 0 ? true : this.current === el.state
      }, this)
    }
  },
}
```

一覧表示テーブルの v-for ディレクティブで使用している todos の部分を computedTodos に置き換えます。

```vue:server/src/components/Tutorial.vue
<tr v-for="item in todos" v-bind:key="item.id">
↓
<tr v-for="item in computedTodos" v-bind:key="item.id">
```

### 8. 文字列の変換処理
最後の仕上げとして「状態変更ボタン」のラベルが数字になっているのを修正します。

```js:server/src/components/Tutorial.vue
export default {
  name: 'Tutorial',
  data () {
    // ... 
  },
  computed: {
    labels () {
      return this.options.reduce(function (a, b) {
        return Object.assign(a, { [b.value]: b.label })
      }, {})
      // キーから見つけやすいように、次のように加工したデータを作成
      // {0: '作業中', 1: '完了', -1: 'すべて'}
    },
    // ...
  },
}
```

Mustache で labels オブジェクトを通すように変更します。

```vue:server/src/components/Tutorial.vue
<button v-on:click="doChangeState(item)">
 {{ labels[item.state] }}
</button>
```

### 完全なvue

```vue
<template>
  <div>
    <label v-for="label in options" v-bind:key="label.value">
      <input type="radio"
             v-model="current"
             v-bind:value="label.value">{{ label.label }}
    </label>
    <table>
      <!-- テーブルヘッダー -->
      <thead>
      <tr>
        <th class="id">ID</th>
        <th class="comment">コメント</th>
        <th class="state">状態</th>
        <th class="button">-</th>
      </tr>
      </thead>
      <tbody>
      <!-- ここに <tr> で ToDo の要素を1行づつ繰り返し表示 -->
      <tr v-for="item in computedTodos" v-bind:key="item.id">
        <th>{{ item.id }}</th>
        <td>{{ item.comment }}</td>
        <td class="state">
          <button v-on:click="doChangeState(item)">
            {{ labels[item.state] }}
          </button>
        </td>
        <td class="button">
          <button v-on:click="doRemove(item)">
            削除
          </button>
        </td>
      </tr>
      </tbody>
    </table>

    <h2>新しい作業の追加</h2>
    <form class="add-form" v-on:submit.prevent="doAdd">
      <!-- コメント入力フォーム -->
      コメント <input type="text" ref="comment">
      <!-- 追加ボタンのモック -->
      <button type="submit">追加</button>
    </form>
  </div>
</template>

<script>
import todoStorage from '../plugins/todoStorage'

export default {
  name: 'Tutorial',
  data () {
    return {
      todos: [],
      uid: 0,
      options: [
        { value: -1, label: 'すべて' },
        { value: 0, label: '作業中' },
        { value: 1, label: '完了' }
      ],
      // 選択している options の value を記憶するためのデータ
      // 初期値を「-1」つまり「すべて」にする
      current: -1
    }
  },
  computed: {
    labels () {
      return this.options.reduce(function (a, b) {
        return Object.assign(a, { [b.value]: b.label })
      }, {})
      // キーから見つけやすいように、次のように加工したデータを作成
      // {0: '作業中', 1: '完了', -1: 'すべて'}
    },
    computedTodos: function () {
      // データ current が -1 ならすべて
      // それ以外なら current と state が一致するものだけに絞り込む
      return this.todos.filter(function (el) {
        return this.current < 0 ? true : this.current === el.state
      }, this)
    }
  },
  created () {
    // インスタンス作成時に自動的に fetch() する
    this.todos = todoStorage.fetch()
    this.uid = this.todos.length
  },
  watch: {
    // オプションを使う場合はオブジェクト形式にする
    todos: {
      // 引数はウォッチしているプロパティの変更後の値
      handler: function (todos) {
        todoStorage.save(todos)
      },
      // deep オプションでネストしているデータも監視できる
      deep: true
    }
  },
  methods: {
    // ToDo 追加の処理
    doAdd: function (event, value) {
      // ref で名前を付けておいた要素を参照
      var comment = this.$refs.comment
      // 入力がなければ何もしないで return
      if (!comment.value.length) {
        return
      }
      // { 新しいID, コメント, 作業状態 }
      // というオブジェクトを現在の todos リストへ push
      // 作業状態「state」はデフォルト「作業中=0」で作成
      this.todos.push({
        id: this.uid++,
        comment: comment.value,
        state: 0
      })
      // フォーム要素を空にする
      comment.value = ''
    },
    // 状態変更の処理
    doChangeState: function (item) {
      item.state = item.state ? 0 : 1
    },
    // 削除の処理
    doRemove: function (item) {
      const index = this.todos.indexOf(item)
      this.todos.splice(index, 1)
    }
  }
}
</script>

<style scoped>
</style>
```

## 参考
- [ToDoリストを作りながら学習しよう！](https://cr-vue.mio3io.com/tutorials/todo.html)