JavaScript
vue.js

[Vue] 親コンポーネントと子コンポーネントで双方向バインディングする

概要

vuejsにて、親コンポーネントから子コンポーネントに対して初期値を与え、子コンポーネントは自身のUIで値を書き換えて、親コンポーネント側のデータも動的に更新させる方法メモ。vuex使えよとかはとりあえず無しで。

以下のGIFの場合、親コンポーネントは子コンポーネントに対して、choiceとtextの初期値をバインドする。子コンポーネントはバインドされたデータを、自身のUIで変更し、変更を親コンポーネントに伝える。親コンポーネントは変更されたデータをリアルタイムに画面に表示する。

fff.gif

2018/04/21 追記
本記事で扱っている仕組みは、.sync修飾子を用いることでより簡略的に実現することができます。が、2.x系の.sync修飾子は1.x系のそれとは異なり、本記事で扱っている内容に近い糖衣構文となっています。個人的にそれが躓きやすいポイントになると思うので、本記事では.sync修飾子を使わずに実現しています。

前提

以下環境で動作確認

debian 8.6
vue 2.5.9
Chrome 63.0

親コンポーネント

<template>
  <div class="parent">
    <h1>親コンポーネント</h1>
    {{ data }}
    <child v-model="data"/>
  </div>
</template>

<script>
  export default {
    data: function() {
      return {
        data: {
          choice: ['A', 'B'],
          text: '初期値',
        }
      }
    },
    components: {child: require('./child')}
  }
</script>

子コンポーネント

<template>
  <div class="child">
    <h2>子コンポーネント</h2>
    <input type="checkbox" value="A" v-model="choice">A
    <input type="checkbox" value="B" v-model="choice">B
    <input type="checkbox" value="C" v-model="choice">C
    <input type="text" v-model="text">
  </div>
</template>

<script>
  export default {
    data: function() {
      return {
        text: '',
        choice: [],
      }
    },
    updated: function() {
      this.$emit('input', {
        choice: this.choice,
        text: this.text,
      })
    },
    mounted: function() {
      this.choice = this.value.choice
      this.text   = this.value.text
    },
    props: {
      value: {
        type: Object,
        required: true,
      },
    },
  }
</script>

解説

v-modelによるデータバインド

まず、カスタムコンポーネント(今回の場合child)に対して、v-modelを定義すると、内部的には以下のように変換がかかる

変換前

<child v-model="data" />

変換後

<child v-bind:value="data"
       v-on:input="data = $event.target.value"
/>

省略記法で書くなら

<child :value="data"
       @input="data = $event.target.value"
/>

つまり、子コンポーネントからは、value属性で親コンポーネントからデータを受け取ることができ、inputメソッドを発火することでデータを更新することができるようになる。

子コンポーネントで初期値の設定

子コンポーネント側では、textとchoiceをそれぞれ定義しておく。

data: function() {
  return {
    text: '',
    choice: [],
  }
},

コンポーネントが読み込まれたタイミングで、親コンポーネントから渡された値でデータを初期化する。

mounted: function() {
  this.choice = this.value.choice
  this.text   = this.value.text
},

もし親コンポーネントからバインドされるv-modelが変化する場合は、mountedでなくwatchで、valueが変化するたびにデータを更新すればよい。

watch: {
  value() {
    this.choice = this.value.choice
    this.text   = this.value.text
  }
},

子コンポーネントでのデータの更新を親コンポーネントに伝搬する

子コンポーネントのUIより、データが書き換わった場合に、親コンポーネントにそれを通知する。前述の通り、v-modelでバインドされたデータに対して、inputメソッドを発火することで親コンポーネントのデータを更新することができる。親コンポーネントのメソッドを呼び出す方法については[Vue] 親コンポーネントのデータ/メソッドを子コンポーネントから使うを参照。

updated: function() {
  this.$emit('input', {
    choice: this.choice,
    text: this.text,
  })
},

これによって、本来は単方向バインディングしか提供されていないVueで、双方向バインディングのようなものを実現できる。