【Vue.js】複数のカウントボタンのカウント数を、Local Storageに保持させたい【Component】

# やりたいこと
前回作った、複数のカウントボタン。
![20200304_vue_after.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575953/ad6a6541-d653-5ef2-e236-c43e73423df9.gif)
参照:[【Vue.js】複数のカウントボタンを、それぞれ押下した数だけカウントさせたい【Component】](https://qiita.com/fukuzono/items/d7366539b3b88e677832)

このそれぞれのボタンでカウントした数字を、リロードしても保持しているように**Local Storageに値を保存したい**と思いました。

# 環境
- Vue.js v2.6.11（CDN）

# 前回までのコード
```html:index.html
  <div id="app">
    <button-counter message="Counter A"></button-counter>
    <button-counter message="Counter B"></button-counter>
    <button-counter message="Counter C"></button-counter>
  </div>
```

```js:js/main.js
  const countComponent = Vue.extend({
    template: '<button @click="countUp">{{ message }}: {{ count }}</button>',
    props: {
      message: {
        type: String,
      },
    },
    data: function () {
      return {
        count: 0,
      };
    },
    methods: {
      countUp: function () {
        this.count++;
      },
    },
  });

  const vm = new Vue({
    el: '#app',
    components: {
      'button-counter': countComponent,
    },
  });
```

# countをLocal Storageに保存してみる
```js:js/main.js
  const buttonCounter = Vue.extend({
    // 省略
    watch: {
      count: {
        handler: function () {
          localStorage.setItem('count', this.count);
        },
        deep: true,
      },
    },
    mounted: function () {
      this.count = JSON.parse(localStorage.getItem('count')) || 0;
    },
  });
```

Local Storageの操作について:[Vue.js - クライアントサイドストレージ](https://jp.vuejs.org/v2/cookbook/client-side-storage.html)

`handler`と`deep`についてはVue.jsのリファレンスに記述がなかったのでこちらを参考にさせていただいています。

値の変更の検出について:[$watchでオブジェクトの変更を監視する方法](https://qiita.com/_Keitaro_/items/8e3f8448d1a0fe281648)(@_Keitaro_ 様)

### 問題発生
現状では`count`にカウントした値が入っていますが、このままLocal Storageに保存しても直近の`count`の値を保存することになり、リロードしてLocal Storageの`count`の値をとってきても、すべてのボタンに直近の値が反映されてしまいます。

![20200307_vue_before.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575953/ed1f7240-9f06-8ccb-5f0b-a089a0fc2a3f.gif)

### 解決案
そこで、**それぞれの値を配列に格納**したいと考えました。
そのために、コンポーネントを複製して再利用していたものを、`v-for`で回して再利用し`index`を取得する方法に変更しました。

# 手順
## 1. コンポーネントの再利用をv-forで回す
```html:index.html
  <div id="app">
    <button-counter></button-counter>
  </div>
```
htmlは複製していたコンポーネントをループを吐き出す場所として一元化しました。

```js:js/main.js
  const buttonCounter = Vue.extend({
    template: '<div><button @click="countUp" v-for="(message, index) in messages" :key="message.id">{{ message }}: {{ count }}</button></div>',
    data: function () {
      return {
        count: 0,
        messages: ['Counter A', 'Counter B', 'Counter C'],
      };
    },
  // 省略
  });
```
jsは`template`に`v-for="(message, index) in messages`と`:key="message.id"`を設定して、`data`オプションにそれぞれのボタンに表示するメッセージの配列を設定しました。

v-forについて:[Vue.js - リストレンダリング](https://jp.vuejs.org/v2/guide/list.html)

templateについて、
> Vue はすべてのコンポーネントに単一のルート要素が必要


ということです。
v-forで回すとルートが複数になるので、`<div></div>`の大枠に入れる必要があるようです。
単一のルート要素について:[Vue.js - コンポネートの基本](https://jp.vuejs.org/v2/guide/components.html#%E5%8D%98%E4%B8%80%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%88%E8%A6%81%E7%B4%A0)

## 2. それぞれのカウント数を管理する配列を設置する
```html:index.html
  <div id="app">
    <button-counter :retentions="retentions"></button-counter>
  </div>
```
```js:main.js

  const buttonCounter = Vue.extend({
    props: {
      retentions: {
        type: Array,
      },
    },
    // 省略
  });

  const vm = new Vue({
    // 省略
    data: {
      retentions: [],
    },
  });
```
親コンポーネントの`data`オプションに`retentions`という配列を設定します。

### 親コンポーネントから子コンポーネントへ値を渡す
親コンポーネントの値を子コンポーネントでも使えるように、**`v-bind`を利用して値を渡します**。
今回はhtmlに`:retentions="retentions"`と記述しています。
そして、子コンポーネントの**`props`オプションで受け取ります**。
`props`オプションでは、型の指定をしています。今回は配列なので`type: Array`を指定しています。
型の種類は以下のリンクをご参照ください。

propsについて:[Vue.js - プロパティ](https://jp.vuejs.org/v2/guide/components-props.html#%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%81%AE%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3)
コンポーネント間のやりとりについて:[Vue.js - プロパティを使用した子コンポーネントへのデータの受け渡し](https://jp.vuejs.org/v2/guide/components.html#%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%9F%E5%AD%90%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%B8%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E5%8F%97%E3%81%91%E6%B8%A1%E3%81%97)


## 3. 配列にそれぞれのカウント数を入れる
```html:index.html
  <div id="app">
    <button-counter @increment="incrementCount" :retentions="retentions"></button-counter>
  </div>
```
`template`で記載している`@click="countUp(index)"`に、`index`を引数に設定します。

### 子コンポーネントから親コンポーネントへ値を渡す
**`v-on`ディレクティブを利用して、htmlに導線を作ります**。今回は`@increment="incrementCount"`と記述しています。

v-onについて:[Vue.js - イベントハンドリング](https://jp.vuejs.org/v2/guide/events.html)

```js:js/main.js
  const buttonCounter = Vue.extend({
    // 省略
    template: '<div><button @click="countUp(index)" v-for="(message, index) in messages" :key="message.id">{{ message }}: {{ retentions[index] }}</button></div>',
    // 省略
    methods: {
      countUp: function (index) {
        this.$emit('increment', index);
      },
    },
  });
```
次に、今回はクリックしたらカウントの処理をさせたいので、既にクリックイベントが設定してある`methods`オプション内の`countUp`関数を使います。
まず、`countUp`に引数(`index`)を設定します。
そして、`$emit`で親の処理を発火させます。
`this.$emit('increment', index);`のように**第一引数でv-onで指定した親コンポーネントのイベントを、第二引数で親に渡したい変数を設定することができます**。

発火させたい処理を、親コンポーネントに記述します。

```js
    methods: {
      incrementCount: function (index) {
        // this.count++;
        const buttonSum = this.retentions[index] + 1;
        this.retentions.splice(index, 1, buttonSum);
      },
    },
```
**`incrementCount`に子コンポーネントからもってきた引数の`index`を渡します。**

コンポーネント間のやりとりについて:[Vue.js - 子コンポーネントのイベントを購読する](https://jp.vuejs.org/v2/guide/components.html#%E5%AD%90%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%AE%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%82%92%E8%B3%BC%E8%AA%AD%E3%81%99%E3%82%8B)

そして、`retentions`配列を`[index]`で指定し、それぞれのボタンのカウント数を`+1`したものを、`splice()`で置き換えするようにしています。

splice()について:[MDN web docs - Array.prototype.splice()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/splice)

`buttonSum`でカウントの処理をするようになったので、今までカウント処理をしてくれていた`this.count++;`はコメントアウトしておきます。


## 4. Local Storageにカウント管理用の配列を保存する
```js:js/main.js
  const vm = new Vue({
    // 省略
    watch: {
      retentions: {
        handler: function () {
          localStorage.setItem('retentions', JSON.stringify(this.retentions));
        },
        deep: true,
      },
    },
  });
```

`watch`オプションに`retention`配列をLocal Storageに保存する処理を記述します。
`retentions`を`JSON.stringify()`で文字列に変更しないと配列ではなく数字で保存されてしまい、意図した動きができないのでご注意ください。

JSON.stringify()について:[MDN web docs - JSON.stringify()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)
Local Storageの操作について:[Vue.js - クライアントサイドストレージ](https://jp.vuejs.org/v2/cookbook/client-side-storage.html)
値の変更の検出について:[$watchでオブジェクトの変更を監視する方法](https://qiita.com/_Keitaro_/items/8e3f8448d1a0fe281648)(@_Keitaro_ 様)

## 5. Local Storageからカウント管理用の配列を呼び出す
```js:js/main.js
  const buttonCounter = Vue.extend({
    // 省略
    template: '<div><button @click="countUp(index)" v-for="(message, index) in messages" :key="message.id">{{ message }}: {{ retentions[index] }}</button></div>',
    // 省略
  });

  const vm = new Vue({
    // 省略
    mounted: function () {
      this.retentions = JSON.parse(localStorage.getItem('retentions')) || [0, 0, 0];
    },
  });
```
`mounted`オプションに`retentions`配列を呼び出すための記述をします。
**呼び出した配列は`retentions`に再び格納**します。
また、`|| [0, 0, 0]`とすることで、Local Storageに値が入っていなかった場合の記述もしておきます。
ここの数字が初期値になっています。

`template`オプションの”Mustache” 構文(描写部分の二重中括弧)の`count`プロパティを、**`retensions`プロパティに変更し、`[index]`で該当のボタンを指定**します。

Local Storageの操作について:[Vue.js - クライアントサイドストレージ](https://jp.vuejs.org/v2/cookbook/client-side-storage.html)

## おまけ
Local Storageに複数のデータを保存できるのか気になったので、ボタンの総カウント数(`total`)も保存するようにしてみました。

```html:index.html
  <div id="app">
    <p>Total Count: {{ total }}</p> <!-- 追記-->
    <button-counter @increment="incrementCount" :retentions="retentions"></button-counter>
  </div>
```

```js:js/main.js
  const vm = new Vue({
    // 省略
    data: {
      total: 0, // 追記
      retentions: [],
    },
    methods: {
      incrementCount: function (index) {
        this.total++; // 追記
        const buttonSum = this.retentions[index] + 1;
        this.retentions.splice(index, 1, buttonSum);
      },
    },
    watch: {
      // ここから追記
      total: {
        handler: function () {
          localStorage.setItem('total', this.total);
        },
        deep: true,
      },
      // ここまで追記
      retentions: {
        handler: function () {
          localStorage.setItem('retentions', JSON.stringify(this.retentions));
        },
        deep: true,
      },
    },
    mounted: function () {
      this.total = JSON.parse(localStorage.getItem('total')) || 0; // 追記
      this.retentions = JSON.parse(localStorage.getItem('retentions')) || [0, 0, 0];
    },
  });
```

# 完成
最終的にこのようになりました。
## コード
```html:index.html
  <div id="app">
    <p>Total Count: {{ total }}</p>
    <button-counter @increment="incrementCount" :retentions="retentions"></button-counter>
  </div>
```

```js:js/main.js
  const buttonCounter = Vue.extend({
    props: {
      retentions: {
        type: Array,
      },
    },
    template: '<div><button @click="countUp(index)" v-for="(message, index) in messages" :key="message.id">{{ message }}: {{ retentions[index] }}</button></div>',
    data: function () {
      return {
        messages: ['Counter A', 'Counter B', 'Counter C'],
      };
    },
    methods: {
      countUp: function (index) {
        this.$emit('increment', index);
      },
    },
  });

  const vm = new Vue({
    el: '#app',
    components: {
      'button-counter': buttonCounter,
    },
    data: {
      total: 0,
      retentions: [],
    },
    methods: {
      incrementCount: function (index) {
        this.total++;
        const buttonSum = this.retentions[index] + 1;
        this.retentions.splice(index, 1, buttonSum);
      },
    },
    watch: {
      total: {
        handler: function () {
          localStorage.setItem('total', this.total);
        },
        deep: true,
      },
      retentions: {
        handler: function () {
          localStorage.setItem('retentions', JSON.stringify(this.retentions));
        },
        deep: true,
      },
    },
    mounted: function () {
      this.total = JSON.parse(localStorage.getItem('total')) || 0;
      this.retentions = JSON.parse(localStorage.getItem('retentions')) || [0, 0, 0];
    },
  });
```

## デモ
![20200307_vue_after.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575953/b654c6e0-e93a-8376-368f-f406afc44de4.gif)

無事、複数のカウントボタンのそれぞれのカウント数をLocal Storageに保存し、リロードしてから保存していた値を取り出すことができました。
複数のデータを保存できることもわかりました。


# 反省
インプットしたつもりで、いざ自分でやってみたいことをやろうとすると、コンポーネント間のデータのやりとりの仕方や親子関係など、自分が理解していない部分があることがよくわかりました。
理解を深めるためにも、積極的に実際に手を動かしたり、自分で動きを考えたりしていきたいと思います。