基礎からメモ: Vue.js CH5-p175 コンポーネントの双方向データバインディング

親子間でデータを受け渡ししたい場合、親から子へはpropsを介して渡す、子から親へはイベントを$emitするという決まりになっていた。 $emitを介さずに子からpropsで渡された値を更新しようとするとエラーになった。これは親のデータを子から簡単に変更できてしまっては問題があるからだ。

しかし子コンポーネントのインプットフォームに入力された値で、親コンポーネントのデータを更新したいといったケースは多い。v-modelを使えば少し簡易にこれが可能になる。

HTMLは単純に親コンポーネントのテンプレートを使っているとする。

<div id="app">
  <my-component></my-component>
</div>

その親のテンプレートでは、インプットフォームを持つ子のコンポーネントを使っているとする。

let myComponent = {
  template: `<div>
   <h4>My Calendar</h4>
   <my-calendar v-model="date"></my-calendar>
   <pre>{{ $data }}</pre>
   </div>`,
   data: function() {
     return { date: "2020" }
   }
};

let app = new Vue({
  el: '#app',
  components: {
    'my-component': myComponent
  }
});

親のdataが変化するか分かるように、親のテンプレートの最後に{{ $data }}を表示している。 子コンポーネントのカスタムタグには、v-model="date"として親コンポーネントのデータを紐付ける。

コンポーネント側では、インプットフォームのタグにv-onでインプットイベント時の呼び出しメソッドと、インプットフォーム内の表示文字列を親から受け取ったdateデータにしている。受け取るデータの変数名はvalueで、デフォルトの名前だが、propsで明示的に受取り、データ型も文字列に指定する。

Vue.component('my-calendar', {
  template: `<input class="my-calendar" v-on:input="updateModel" v-bind:value="value">`,
  props: { value: String },
   methods: {
    updateModel: function(event) {
      this.$emit('input', event.target.value)
    }
  }
});

これでインプットフォームに、親コンポーネントdateデータ「2020」が初期値として表示され、何かを入力すると子コンポーネントupdateModelメソッドが呼ばれ、$emitを介して親のdateデータも更新される。updateModelメソッドにはeventオブジェクトが渡されるので、$emitの第二引数をevent.target.valueとすれば、親のdateデータに入力した値が放り込まれる。

この仕組みにはもうすこし簡単な書き方がある。子コンポーネントのメソッドを介さず、v-on:inputに直接$emitを書いてしまう方法だ。イベントオブジェクトは$eventと書く。

Vue.component('my-calendar', {
  template: `<input class="my-calendar" v-on:input="$emit('input', $event.target.value)" v-bind:value="value">`,
  props: { value: String }
});

v-modelで使うプロパティ名やイベント名をカスタマイズ

コンポーネント中のv-modelのデフォルトでは、プロパティ名をvalue、イベント名をinputとして使う。ただしこの名前をmodelオプションで任意の名前に変更できる。子コンポーネントpropsで受け取る変数名をcurrent、イベント名をchangeに変えてみる。

Vue.component('my-calendar', {
  template: `<input class="my-calendar" v-on:change="$emit('change', $event.target.value)" v-bind:value="current">`,
  model: {
    prop: 'current',
    event: 'change'
  },
  props: { current: String }
});

.syncによる双方向データバインディング

v-modelでは単一の属性しか同期できない。複数の属性を同期させるには.sync修飾子を使う。ただし子側からはあくまで$emitでのイベント発火を介して親のデータの同期をする。

コンポーネントで子コンポーネントのカスタムタグを使う際に、v-bind.sync修飾子で複数のデータを紐付けておく。

let myComponent = {
  template: `<div>
   <my-component 
     v-bind:name.sync="name" 
     v-bind:hp.sync="hp"></my-component>
   <pre>{{ $data }}</pre>
   </div>`,
   data: function() {
     return { 
      name: "スライム",
      hp: 100
     }
   }
};

let app = new Vue({
  el: '#app',
  components: {
    'my-component': myComponent
  }
});

子側では、算出プロパティのセッターとゲッターを使って、v-modelで関連付けたデータの操作をする。

Vue.component('my-component', {
  template: `<div class="my-component">
    <p>名前: {{ name }}, HP: {{ hp }}</p>
    <p>名前 <input v-model="localName"></p>
    <p>HP <input size="5" v-model.number="localHp"></p>
  </div>`,
  props: { 
    name: String,
    hp: Number 
  },
  computed: {
    localName: {
      get: function() { return this.name },
      set: function(val) { this.$emit('update:name', val) }
    },
    localHp: {
      get: function() { return this.hp },
      set: function(val) { this.$emit('update:hp', val) }
    }
  }
});

名前とHPを変更する2つのインプットフォームに値を入力すると、親が保持する対応データが同期する。