こんな方に向けて書きました
- 自作コンポーネントで
v-model
したいけどやり方がわからない -
Avoid mutating a prop directly
のメッセージが出た! - 「イベントでやり取りする」ってどういうことよ
同様の説明をしている記事を見つけたのでこちらも参考にしてみてください: https://se-tomo.com/2018/11/03/vue-js-コンポーネント間の通信/
おさえておきたい基本のしくみ
Vueは気軽に使い始められるところがいいところで、ガイドを読み飛ばしてもそれっぽく使えてしまいます。
ですがガイドには結構重要なことが書いてあるので、本当は全部読んだ方がいいです(そういう私も全部は読んでいませんが)。
一方で完全な初学者〜ちょっと使い慣れたくらいでは、ガイドをちゃんと理解しながら読むのは難しいかもしれません。
そこでデータバインドの基本を押さえるにあたって、さしあたり必要な項目をまとめてみました。
私は算出プロパティとかすっかりスルーしていましたが、使い始めるとかなり便利でした・・・もっと早く知りたかった(ガイド読み飛ばしていたのが悪いですね。はい。)
v-bind
, :value
親 -> 子にデータを受け渡す際、親側で記述します。親からデータを渡すための窓口のイメージです。
v-bind:プロパティ名
もしくは:プロパティ名
という書き方をします。プロパティ名は、後述する子要素のprops
の変数名のことです。
props
親 -> 子にデータを受け渡す際、子側で記述します。子がデータを受け取るための窓口のイメージです。
v-on
, @input
子で何らかの入力やアクション(イベントという)があった際、どんな処理をするのかを、親側で記述します。
v-on:イベント名
もしくは@イベント名
という書き方をします。
$emit
子でクリックや文字入力など(イベントという)があったことを親に通知します。
この仕組みを使って 子 -> 親にデータを受け渡します。 実はこれがVueの一見面倒なところであり、とても大事な箇所です。
$emit
の際には「何がおきたか(イベント名)」と「引数」を渡すことができます。
引数にはなんでも渡せてしまうので、何を渡すのかを親子間で取り決めておく必要があります。
v-model
<input v-model="foo">
みたいな形でおなじみv-model
。
使う際にはとっても楽ちんですが、自作コンポーネントでこれをやるにはきちんとデータバインドの基本がわかっていないとできません。
v-model="foo"
は、v-bind:value="foo" @input="foo = $event"
と同じです。
つまり、親コンポーネントがfoo
というデータを持っていて、
- 子コンポーネントの
value
propにfoo
を渡す - 子コンポーネントの
input
イベント時に、foo
にイベント引数を代入する
という2つを行います。
value
とinput
という名称は基本的には固定ですが、子コンポーネント側で変更することも一応可能です。
data
コンポーネント自身が持つデータです。
props
は親から渡されたデータをそのまま格納するだけで変更しないのが原則ですので、実質的にdata
が唯一のデータです。
テンプレートでデータバインドするには、基本的にはこのdata
か次のcomputed
を使います。
computed
data
やprops
を加工し、あたかも通常のdata
かのように振る舞うメソッドのこと。正式には算出プロパティと言います。
実態はdata
またはprops
のgetterとsetterです。テンプレートで使うときは、通常のdata
と全く同じように使います。
使い道はこんな感じ:
- フォーマットなど、
data
に何らかの加工をしてからテンプレートで表示する - 算出setterを使うことで、代入するだけでevent upする
props
を中継する算出プロパティを使って、親から渡されたデータをそのまま表示 & 入力を直ちにevent upする
3番目の方法を使ってデータバインドすると色々嬉しいことがあります。あとで実例を挙げて解説します。
watch
data
やprops
の変更を監視して、指定したメソッドを実行します。
computed
も似たような感じですが、watch
の方が汎用性が高いです。
ですがVue公式ガイドではwatch
よりもcomputed
を推してます。
v-bind.sync
, $emit('update:value')
双方向バインディングっぽいことをするためのディレクティブとevent upです。初めての方には難しいのでいったん後回し。
データバインドの全体像をつかもう
Vueの親子コンポーネント間でデータをやりとりするときの鉄則は Events Up, Props Down です。
すなわち、
- 親 -> 子へデータを受け渡す際には
v-bind
とprops
を使う(props down) - 子 -> 親へデータを受け渡す際には
$emit
とv-on
を使う(events up)
後者をサボり、 直接props
をいじるとデバッグコンソールで怒られます。
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "value"
vue.runtime.esm.js:601
絵にするとこんな感じ。v-model
を多用しているとわかりにくいですが、完全な双方向バインディングではなく一方通行のデータの流れが見えてきます。
データバインドのパターンあれこれ
では、実際にコードでデータバインドの基本を見ていきましょう。
まずは基本形
propで受け、computedで入出力し、変更をemitし、親はイベントで受けるパターンです。
上の絵のようなコンポーネントを作成してみます。
外側の枠がParent.vue
, 内側の枠がChild.vue
に対応します。
<template>
<div class="parent">
<Child :value="parentMsg" @input="parentMsg = $event"/>
<div>入力内容: {{ parentMsg }}</div>
<button @click="onClickRandomButton">ランダム</button>
</div>
</template>
<script>
import Child from "./Child.vue";
export default {
name: "Parent",
components: {
Child
},
data: function() {
return {
parentMsg: "",
fruits: [
"オレンジ",
"バナナ",
"グレープフルーツ",
"クランベリー",
"レモン",
"マンゴー",
"キウイフルーツ"
]
};
},
methods: {
onClickRandomButton: function() {
const randomIndex = Math.floor(Math.random() * this.fruits.length);
this.parentMsg = this.fruits[randomIndex];
}
}
};
</script>
<template>
<div class="child">
<label>
果物:
<input v-model="msg">
</label>
</div>
</template>
<script>
export default {
name: "Child",
props: {
value: String
},
computed: {
msg: {
get: function() {
return this.value;
},
set: function(newValue) {
this.$emit("input", newValue);
}
}
}
};
</script>
Child
ではmsg
算出プロパティをテンプレートで使っています。msg
はget時にはpropで渡された値をそのまま中継しますが、set時にはpropに代入するのでなく$emit("input", newValue)
でイベント発火し親コンポーネントに入力があったことを伝えています。
この方法だとテンプレート内でv-model
を使うことができるためおすすめです。
いったんデータを蓄える
propで受け、watchでdataに流し込み、dataをデータバインドし、入力時のイベントでemitし、親はイベントで受けるパターンです。
何らかの理由で算出プロパティを使いたくない場合はこちら。
上の絵のように、子コンポーネントでの入力を直ちに親コンポーネントに反映せず、
ボタンクリックで親に変更を通知してみます。
算出プロパティを使うと直ちに反映されてしまうので、いったんデータを蓄えるこちらのパターンが適しています。
<template>
<div class="parent">
<Child :value="parentMsg" @input="parentMsg = $event"/>
<div>入力内容: {{ parentMsg }}</div>
<button @click="onClickRandomButton">ランダム</button>
</div>
</template>
<script>
import Child from "./Child.vue";
export default {
name: "Parent",
components: {
Child
},
data: function() {
return {
parentMsg: "",
fruits: [
"オレンジ",
"バナナ",
"グレープフルーツ",
"クランベリー",
"レモン",
"マンゴー",
"キウイフルーツ"
]
};
},
methods: {
onClickRandomButton: function() {
const randomIndex = Math.floor(Math.random() * this.fruits.length);
this.parentMsg = this.fruits[randomIndex];
}
}
};
</script>
<template>
<div class="child">
<label>
果物:
<input v-model="msg">
</label>
<div>
<button @click="onClickButton">反映↓</button>
</div>
</div>
</template>
<script>
export default {
name: "Child",
props: {
value: String
},
data: () => ({
msg: ""
}),
methods: {
onClickButton: function() {
this.$emit("input", this.msg);
}
},
watch: {
value: function(newValue) {
this.msg = newValue;
}
}
};
</script>
テンプレートで使うのはあくまでもmsg
とし、propの変更をwatchにより監視してmsg
を更新する方法です。
propの変更は常時watchされるので、「ランダム」ボタンを押した際には即時子コンポーネント側のdataは変更されます。
一方子コンポーネントの変更は監視されておらず、入力があったタイミングでinput
イベントが発火されるようには指定していない(入力とイベント発火を切り離している)ため、「反映」ボタンをクリックしたタイミングで初めて親コンポーネントに入力内容が通知されます。
(非推薦) 親のデータを直接書き換える
propでObjectを受け、Objectを書き換えるパターンです。
親コンポーネントから受けとったpropがオブジェクトの場合には、オブジェクトの内容を子で書き換えてもVueに怒られたりしません。
怒られないという事実に気が付いてしまい、結構楽なので使ってみようかと思ったこともありましたが、推薦はされないようです。
でもちゃんとリアクティブになってるんだよなぁ。
これがやりたいのは、「たくさんのキー・値を含むオブジェクトなので、個別にpropを受け取るのではなく、オブジェクト丸ごと受け取ってデータバインドしたい」みたいなケースだと思います。
そういう場合に対応できるのが、次項の「syncパターン」です。
親からObjectを受け取って中身を書き換えたい: syncパターン
たくさんのキー・値を含むオブジェクトだからといって、オブジェクト丸ごと受け取り、propを直接書き換えてしまうのはお行儀がいいとは言えません。
面倒でも、個別のプロパティをpropsで宣言し、データの変更時にevent upするのが本来の方法とされています。
ですがそれだと下のように、たくさんデータバインドしなければならず面倒です。
<template>
<div class="child">
<label class="item">氏名:
<input v-model="nameComputed">
</label>
<label class="item">好きな果物:
<input v-model="fruitComputed">
</label>
<label class="item">好きな花:
<input v-model="flowerComputed">
</label>
</div>
</template>
<script>
export default {
name: "Child",
props: {
name: String,
fruit: String,
flower: String
},
略
}
Child.vue
で定義した3つのprop(name, fruit, flower)に対して、Parent.vue
でそれぞれデータバインドします:
<template>
<div class="parent">
<Child
:name="parentData.name"
@updated-name="parentData.name = $event"
:fruit="parentData.fruit"
@updated-fruit="parentData.fruit = $event"
:flower="parentData.flower"
@updated-flower="parentData.flower = $event"
/>
<div>入力内容:
<div>氏名: {{parentData.name}}</div>
<div>好きな果物: {{parentData.fruit}}</div>
<div>好きな花: {{parentData.flower}}</div>
</div>
</div>
</template>
<script>
import Child from "./Child.vue";
export default {
name: "Parent",
components: {
Child
},
data: function() {
return {
parentData: {
name: "",
fruit: "",
flower: ""
}
};
}
};
</script>
そんなとき、子コンポーネント側で以下の条件を満たせば、v-bind.sync
という省略記法が使えます。
- events upするときのイベント名を
updated:プロパティ名
としておく
これで、親 -> 子にデータバインドする際の記述を大幅に省略できます。
残念ながら子コンポーネント側にはたくさんのpropsとcomputed dataを宣言する必要がありますが。
<template>
<div class="child">
<label class="item">氏名:
<input v-model="nameComputed">
</label>
<label class="item">好きな果物:
<input v-model="fruitComputed">
</label>
<label class="item">好きな花:
<input v-model="flowerComputed">
</label>
</div>
</template>
<script>
export default {
name: "Child",
props: {
name: String,
fruit: String,
flower: String
},
computed: {
nameComputed: {
get: function() {
return this.name;
},
set: function(newValue) {
this.$emit("update:name", newValue);
}
},
fruitComputed: {
get: function() {
return this.fruit;
},
set: function(newValue) {
this.$emit("update:fruit", newValue);
}
},
flowerComputed: {
get: function() {
return this.flower;
},
set: function(newValue) {
this.$emit("update:flower", newValue);
}
}
}
};
</script>
3つのprop(name, fruit, flower)それぞれのv-bind
とv-on
を書かず、parentData
をまるごとデータバインドしているかのように書くことができます:
<template>
<div class="parent">
<Child v-bind.sync="parentData" />
<div>入力内容:
<div>氏名: {{parentData.name}}</div>
<div>好きな果物: {{parentData.fruit}}</div>
<div>好きな花: {{parentData.flower}}</div>
</div>
</div>
</template>
あとは一緒
参考サイトなど
環境
- vue: 2.5.22
- vue-cli-service: 3.4.0