TL;DR
- 自分が実装したカスタム(子)コンポーネントに
v-model
を書き、データの双方向データバイディングができる。 - 基本的にはデフォルトでは、そのカスタム(子)コンポーネントで、
value
のキーのpropsでデータを受け取り、input
のイベント名で変更したいデータをemit
すれば、親のほうでv-model
で渡しているデータが更新される。 - そのカスタム(子)コンポーネントで、propsで受け取るデータのキーだったり、イベント名を変更したい場合は、
model
プロパティに、変更したいデータのプロパティと変更したいときの使うカスタムイベント名を定義する。
はじめに
- メディア運営会社のエンジニアとして働いています。メディアのコンテンツを入稿するツール(以下: ダッシュボード)をVuejs(Nuxtjs)で開発しているとき、自分が実装した子コンポーネントを呼び出した親で使うときに、双方向データバイディングについて辛く感じ、いい方法をググっていたところ、自分が実装したカスタムコンポーネントでも
v-model
を使えることを知って、開発が楽になったので、学んだことをここに書きます。 - 学ぶ前と学んだ後でどれだけコードが変わるのかの before / afterも書きます。
- UIフレームワークはvuetifyを使用しています。
そもそもv-modelって何?どうやって動いているの?
- はじめに
v-model
についておさらいすると、双方向データバインディングとして紹介されています。具体的な使用例としては、メールアドレスやチェックボックスなどにあるフォームが多いです。下だとinputで書いた内容がそのままemail
に反映されます。 - 最初見たときは、これどう動いているんだ...?と思ったのですが、公式のドキュメントによると、
v-model
はvalue
プロパティを通して、フォームのinput
やtextarea
、select
要素にデータを渡し、一方でそれぞれの要素が発行するinput
やchange
イベントでvalue
プロパティで渡したデータを更新しているというものです。 - v-model
- コンポーネントでv-modelを使う
<template>
<div class="example-form">
<!-- 下2つは同じこと -->
<input v-model="email" type="text" />
<input :value="email" @input="email = $event.target.value" type="text">
<p> {{ email }} <p>
</div>
</template>
<script>
export default {
data() {
return {
email: '',
}
},
}
</script>
さらに、公式のv-model を使ったコンポーネントのカスタマイズを読むと、「デフォルトではコンポーネントにある v-model
は value
をプロパティとして、input
をイベントして使います。」とあります。
言い換えると、input
や textarea
要素も一つのコンポーネントと仮定して、value
プロパティでデータをもらったり、input
イベントで経由でもらったデータを更新しているとしたら、どんなコンポーネントでも同じこと、つまりvalue
プロパティでデータをもらって、input
イベントでもらったデータを更新すれば、input
や textarea
要素で使用しているようなv-model
が使えるということになります。
下の例だと custom-input
というカスタムコンポーネントは、更新するデータをvalue
で受け取り、データの更新時に input
イベントを発行していることで、呼び出しているほうでv-model
を使用することができます。
<custom-input v-model="searchText" />
<template>
<input
:value="value"
@input="$emit('input', $event.target.value)"
>
</template>
<script>
export default {
name: 'custom-input',
props: ['value'],
}
</script>
でも、v-model
を使用するのに、別のプロパティだったり、違うイベント名を使いたいというケースがあると思います。
そんなときは、v-model を使ったコンポーネントのカスタマイズにあるように、model
プロパティを使用すれば大丈夫です。
下の例だと、デフォルトの value
の代わりに checked
が使用され、input
の代わりに change
が使用されています。
<base-checkbox v-model="lovingVue"></base-checkbox>
<template>
<input
type="checkbox"
:checked="checked"
@change="$emit('change', $event.target.checked)"
>
</template>
<script>
export default {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean,
},
}
</script>
色々と書きましたが、実務でどう役に立ったかを書いていきたいと思います。
実装するカスタムコンポーネント
Before
<template>
<v-dialog :value="_dialog" max-width="500" @click:outside="closeDialog">
<v-card>
<v-card-title class="headline grey lighten-2 text-center" primary-title>
v-model
</v-card-title>
<v-card-actions>
<v-btn color="accent" text @click="closeDialog">閉じる</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
props: {
dialog: {
type: Boolean,
default: false,
},
},
computed: {
_dialog: {
get() {
return this.dialog
},
set(val) {
this.$emit('close:dialog')
},
},
},
methods: {
closeDialog() {
this._dialog = false
},
},
}
</script>
<template>
<v-container fill-height>
<v-layout align-center>
<v-flex class="text-center">
<r-custom-dialog :dialog="dialog" @close:dialog="closeDialog()" />
<v-btn color="primary" @click="openDialog">CLICK ME</v-btn>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import RCustomDialog from '~/components/page-organisms/articles/r-custom-dialog.vue'
export default {
components: {
RCustomDialog,
},
data() {
return {
dialog: false,
}
},
methods: {
openDialog() {
this.dialog = true
},
closeDialog() {
this.dialog = false
},
},
}
</script>
vuejsでは親からpropsでもらったデータを子コンポーネント内で変更したときにエラーが出てきます。
エラーを回避するために、子コンポーネントの computed
内でprops
に代わるプロパティを用意し、その代用されたプロパティに対して変更を加えています。そして、そのプロパティの内容が変更されたときに、カスタムイベントを発火するようにしています。
改善したいなと感じたのが、親が子に渡したデータを変更していることと子のほうで代用のプロパティを用意していること。親は子供から変更されたデータを受け取るようにしたらいいなと思うのと、親と子でデータを無理やり同期しているように感じるのと、子のほうでわざわざgetterとsetterを用意するのは手間がかかるなと思いました。
そんな中、カスタムコンポーネントで v-model
が使えるとわかり、コードを書き換えると... ↓
After
<template>
<v-dialog :value="dialog" max-width="500" @click:outside="closeDialog">
<v-card>
<v-card-actions>
<v-btn color="accent" text @click="closeDialog">
閉じる
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
model: {
prop: 'dialog',
event: 'change-dialog',
},
props: {
dialog: {
type: Boolean,
default: false,
},
},
methods: {
closeDialog() {
this.$emit('change-dialog', false)
},
},
}
/* デフォルトだとこう書く。
export default {
props: {
value: {
type: Boolean,
required: true,
},
},
methods: {
closeDialog() {
this.$emit('input', false)
},
},
}
*/
</script>
<template>
<div class="model-examples-index">
<r-custom-dialog v-model="dialog" />
<v-btn @click="openDialog()" />
</div>
</template>
<script>
import RCustomDialog from '~/components/page-organisms/articles/r-custom-dialog.vue'
export default {
data() {
return {
dialog: false,
}
},
methods: {
openDialog() {
this.dialog = true
},
},
}
</script>
まさにこんなのが欲しかったなと思いました。
無理やり感も薄れ、双方向データバイディングができて、親は子がデータに変更が加わったことを気にせず、ただただ変更が加わったデータを扱えばよくなりました。子のほうで、余分な computed
も用意しなくてもいいのですっきりしました。
ポイントは
-
model
プロパティに親から渡されるデータの名前をprop
に、変更が加わるときのイベント名をchange-dialog
にします。 - しっかりと
model
プロパティのprop
に指定するデータ がprops
の中で定義されていること。
まとめ
- カスタムコンポーネントで
v-model
が書ける話をまとめました。これを知ってからは、無駄なcomputed
のgetterやsetterを書かなくても大丈夫ですし、親と子でしっかり双方向データバインディングができているので、すっきり書けたかなと思っています。よかったら、いいねしていただけると嬉しいです!