やりたいこと
前回作った、複数のカウントボタン。
参照:【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に保存し、リロードしてから保存していた値を取り出すことができました。
複数のデータを保存できることもわかりました。
反省
インプットしたつもりで、いざ自分でやってみたいことをやろうとすると、コンポーネント間のデータのやりとりの仕方や親子関係など、自分が理解していない部分があることがよくわかりました。
理解を深めるためにも、積極的に実際に手を動かしたり、自分で動きを考えたりしていきたいと思います。