やりたいこと
前回作った、複数のカウントボタン。

参照:【Vue.js】複数のカウントボタンを、それぞれ押下した数だけカウントさせたい【Component】
このそれぞれのボタンでカウントした数字を、リロードしても保持しているようにLocal Storageに値を保存したいと思いました。
環境
- Vue.js v2.6.11(CDN)
前回までのコード
  <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>
  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に保存してみる
  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 - クライアントサイドストレージ
handlerとdeepについてはVue.jsのリファレンスに記述がなかったのでこちらを参考にさせていただいています。
値の変更の検出について:$watchでオブジェクトの変更を監視する方法(@_Keitaro_ 様)
問題発生
現状ではcountにカウントした値が入っていますが、このままLocal Storageに保存しても直近のcountの値を保存することになり、リロードしてLocal Storageのcountの値をとってきても、すべてのボタンに直近の値が反映されてしまいます。
解決案
そこで、それぞれの値を配列に格納したいと考えました。
そのために、コンポーネントを複製して再利用していたものを、v-forで回して再利用しindexを取得する方法に変更しました。
手順
1. コンポーネントの再利用をv-forで回す
  <div id="app">
    <button-counter></button-counter>
  </div>
htmlは複製していたコンポーネントをループを吐き出す場所として一元化しました。
  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 - リストレンダリング
templateについて、
Vue はすべてのコンポーネントに単一のルート要素が必要
ということです。
v-forで回すとルートが複数になるので、<div></div>の大枠に入れる必要があるようです。
単一のルート要素について:Vue.js - コンポネートの基本
2. それぞれのカウント数を管理する配列を設置する
  <div id="app">
    <button-counter :retentions="retentions"></button-counter>
  </div>
  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 - プロパティ
コンポーネント間のやりとりについて:Vue.js - プロパティを使用した子コンポーネントへのデータの受け渡し
3. 配列にそれぞれのカウント数を入れる
  <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 - イベントハンドリング
  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で指定した親コンポーネントのイベントを、第二引数で親に渡したい変数を設定することができます。
発火させたい処理を、親コンポーネントに記述します。
    methods: {
      incrementCount: function (index) {
        // this.count++;
        const buttonSum = this.retentions[index] + 1;
        this.retentions.splice(index, 1, buttonSum);
      },
    },
incrementCountに子コンポーネントからもってきた引数のindexを渡します。
コンポーネント間のやりとりについて:Vue.js - 子コンポーネントのイベントを購読する
そして、retentions配列を[index]で指定し、それぞれのボタンのカウント数を+1したものを、splice()で置き換えするようにしています。
splice()について:MDN web docs - Array.prototype.splice()
buttonSumでカウントの処理をするようになったので、今までカウント処理をしてくれていたthis.count++;はコメントアウトしておきます。
4. Local Storageにカウント管理用の配列を保存する
  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()
Local Storageの操作について:Vue.js - クライアントサイドストレージ
値の変更の検出について:$watchでオブジェクトの変更を監視する方法(@_Keitaro_ 様)
5. Local Storageからカウント管理用の配列を呼び出す
  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 - クライアントサイドストレージ
おまけ
Local Storageに複数のデータを保存できるのか気になったので、ボタンの総カウント数(total)も保存するようにしてみました。
  <div id="app">
    <p>Total Count: {{ total }}</p> <!-- 追記-->
    <button-counter @increment="incrementCount" :retentions="retentions"></button-counter>
  </div>
  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];
    },
  });
完成
最終的にこのようになりました。
コード
  <div id="app">
    <p>Total Count: {{ total }}</p>
    <button-counter @increment="incrementCount" :retentions="retentions"></button-counter>
  </div>
  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];
    },
  });
デモ
無事、複数のカウントボタンのそれぞれのカウント数をLocal Storageに保存し、リロードしてから保存していた値を取り出すことができました。
複数のデータを保存できることもわかりました。
反省
インプットしたつもりで、いざ自分でやってみたいことをやろうとすると、コンポーネント間のデータのやりとりの仕方や親子関係など、自分が理解していない部分があることがよくわかりました。
理解を深めるためにも、積極的に実際に手を動かしたり、自分で動きを考えたりしていきたいと思います。

