【Vue.js 再入門】 v-model を正しく理解して親子間コンポーネントのデータ伝播をマスターする

## はじめに

>:warning: **本稿は Vue 2.x が対象になります。Vue 3 に関する記事は[こちら](https://qiita.com/toshifumiimanishi/private/7a276d5f3c60fbaf986d)を参照ください。**

Vue.js のコンポーネント間のデータのやりとりについて、親コンポーネントから子コンポーネントへデータを渡すには、`v-bind` 属性と `props` オプションを利用します。これにより親コンポーネント側でデータを変更すれば、子コンポーネントの `props` 経由で渡されたデータも変更されます。しかし、子コンポーネント側で `props` 経由で渡されたデータを直接変更することは非推奨です。しかし、子コンポーネントの変更を親コンポーネントへ反映したい場合があります。たとえば、再利用性のある入力フォームのコンポーネントを親子間でやりとりするケースです。この問題を解決するには `v-model` を正しく理解する必要があります。本稿で紹介するコードは CodeSandbox にて共有します。

### サンプルコード

**▼ v-model を利用しないサンプルコード**
[![Edit 【Vue.js 再入門】v-model を正しく理解して親子コンポーネント間のデータ伝播をマスターする](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/vuejs-zairumenv-model-wozhengsikulijiesiteqinzikonponentojiannodetachuanbowomasutasuru-3rkwv?fontsize=14&hidenavigation=1&theme=dark)

**▼ v-model を利用したサンプルコード**
[![Edit 【Vue.js 再入門】v-model を正しく理解して親子コンポーネント間のデータ伝播をマスターする（v-model ver.）](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/vuejs-zairumenv-model-wozhengsikulijiesiteqinzikonponentojiannodetachuanbowomasutasuruv-model-ver-3zhbv?fontsize=14&hidenavigation=1&theme=dark)

## <ruby>御復習い<rp>(</rp><rt>おさらい</rt><rp>)</rp></ruby>
`v-model` は双方向データバインディングを実現するディレクティブです。双方向データバインディングとは、ビューで変更があった場合、その値を Vue インスタンスのデータとして更新します。逆に Vue インスタンスのデータに変更があった場合はビューを再レンダリングします。

```vuejs
<input v-model="message" />
```

`v-model` はユーザーの入力イベントにおいてデータを更新するための糖衣構文です。たとえば、以下はテキスト入力欄の糖衣構文の事例です。

```vuejs:テキスト入力欄の糖衣構文
<input
  :value="message"
  @input="message = $event.target.value"
/>
```

また、`v-model` は `<input>` 要素の `type` 属性の値に応じて異なる属性のバインディング、イベントを伝播します。

- テキストと複数行テキストは `value` 属性と `input` イベント
- チェックボックスとラジオボタンは `checked` 属性と `change` イベント
- 選択フィールドは `value` 属性と `change` イベント

さらにコンポーネントの `v-model` の糖衣構文は以下のようになります[^1]。

```vuejs:コンポーネントの糖衣構文
<CustomTextField
  :value="message"
  @input="message = $event"
/>
```

## 本題

子コンポーネントの変更を親コンポーネントへ反映する方法をテキスト入力欄の事例で紹介します。

### テキスト入力欄

まずは親コンポーネントについて説明します。`v-bind` 属性経由で子コンポーネントにデータ（`message`）を渡すための準備をします。`props` は親コンポーネントの `v-bind` 属性経由で渡します。

```vuejs:親コンポーネント
<!-- コードは一部省略しています。 -->
<CustomTextField :value="message" />

<script>
export default {
  data() {
    return {
      message: "",
    };
  },
};
</script>
```

`v-bind` 属性経由で渡されたデータは子コンポーネントの `props` オプションで定義します。

```vuejs:子コンポーネント
<script>
// props オプションの定義
export default {
  props: {
    value: {
      type: String,
    },
  },
};
</script>
```

上記のように `props` オプションを定義すれば、コンポーネントをインスタンス化したときにオブジェクトのプロパティとして利用できます。つまり、`data` と同様にテンプレート中で展開できます。それでは子コンポーネントの `value` 属性に `props` で定義した`value` を `v-bind` 属性でバインディングしましょう。

```vuejs:子コンポーネント
<input :value="value" />
```

子コンポーネントから親コンポーネントへの通信では、`$emit` メソッドでイベントを送出します。それでは子コンポーネントの入力イベントを親コンポーネントへ通信するための準備をしましょう。

```vuejs:子コンポーネント
<input
  :value="value"
  @input="$emit('input', $event.target.value)"
/>
```

`input` イベントをハンドリングして、`$emit` メソッドで親コンポーネントにイベントを送出します。`$emit` メソッドの第 2 引数には子コンポーネントで入力された値を指定します。これにより親コンポーネントの `v-on` ディレクティブを使ってイベントを受け取る際、子コンポーネントで入力された値を取得できます。

```vuejs:親コンポーネント
<CustomTextField
  :value="message"
  @input="message = $event"
/>
```

親コンポーネント側でも `input` イベントをハンドリングして、子コンポーネントで入力された値を親コンポーネントで管理している `message` に代入します。これで子コンポーネントの変更を親コンポーネントへ反映できます。しかし、このままでは少し冗長なのでもう少しスッキリした書き方があります。それが `v-model` です。`v-model` をカスタムコンポーネントで利用する場合、上記の糖衣構文になります。

```vuejs:親コンポーネント
<CustomTextField v-model="message" />
```

ちなみに子コンポーネントでも同様に `v-model` を利用したい場合は、算出プロパティの `getter` 関数と `setter` 関数を利用します。算出プロパティはデフォルトでは `getter` 関数のみですが、必要があれば `setter` 関数も使えます。

```vuejs:子コンポーネント
<!-- v-model を利用した場合 -->
<template>
  <input v-model="inputedValue" />
</template>

<script>
export default {
  props: {
    value: {
      type: String,
    },
  },
  computed: {
    inputedValue: {
      get() {
        return this.value;
      },
      set(newValue) {
        this.$emit("input", newValue);
      },
    },
  },
};
</script>
```

`input` イベントでハンドリングして、`v-bind` 属性でバインディングした `value` 属性を setter 関数の引数に渡し、`$emit` メソッドで親コンポーネントに入力されたデータを伝播します。以上で v-model の親子間コンポーネントのデータ伝播に関する説明を終えますが、これがテキスト入力欄ではなく他の `type` 属性の `<input>` 要素ではどうすればよいかをチェックボックスを事例に説明します。

### チェックボックス

チェックボックスがチェックされているかどうかの `checked` 属性値は親コンポーネントで管理します。子コンポーネントは `props` 経由で `checked` 属性値を受け取ります。チェックボックスの ON / OFF の `change` イベント時に `checked` 属性値を親に伝搬します。

```vuejs:子コンポーネント
<template>
  <label>
    <input type="checkbox" @change="$emit('change', $event.target.checked)" />
    {{ checked }}
  </label>
</template>

<script>
export default {
  props: {
    checked: {
      type: Boolean,
    },
  },
};
</script>
```

親コンポーネントは `checked` 属性を `v-bind` 属性経由で子コンポーネントに伝搬します。また、子コンポーネントの `change` イベントをハンドリングします。イベントハンドラの引数を `data` オプションで管理している `checked` に代入します。

```vuejs:親コンポーネント
<CustomCheckbox
  :checked="checked"
  @change="checked = $event"
/>
```

前例のテキスト入力欄では上記のコードを `v-model` で置換しましたが、コンポーネントの `v-model` の糖衣構文とは異なります。では `v-model` に置換できないかというと答えは置換できます。子コンポーネントに `model` オプションを指定します。

```vuejs:子コンポーネント
<script>
export default {
  model: {
    prop: "checked",
    event: "change",
  },
  props: {
    checked: {
      type: Boolean,
    },
  },
};
</script>

```

`model` オプションの `props` と `event` をチェックボックスの続生とイベントに合わせることで、親コンポーネントで `v-model` に置換できます。

```vuejs:親コンポーネント
<CustomCheckbox v-model="checked" />
```

ちなみに、子コンポーネントで `v-model` を利用したい場合は、前例のテキスト入力欄と同様に実装になります。

```vuejs:子コンポーネント
<template>
  <label>
    <input type="checkbox" v-model="inputedValue" />
    {{ checked }}
  </label>
</template>

<script>
export default {
  model: {
    prop: "checked",
    event: "change",
  },
  props: {
    checked: {
      type: Boolean,
    },
  },
  computed: {
    inputedValue: {
      get() {
        return this.checked;
      },
      set(newValue) {
        this.$emit("change", newValue);
      },
    },
  },
};
</script>
```


## さいごに
いかがでしょうか。どちらも `v-model` を正しく理解していなければ一見なにをやっているのか把握しにくいかと思います。`v-model` に限らず正しく仕様を理解することは重要です。本稿がどなたかの一助になれば幸甚です。

## 参考文献

- [フォーム入力バインディング — Vue.js](https://jp.vuejs.org/v2/guide/forms.html)
- [コンポーネントの基本 — Vue.js](https://jp.vuejs.org/v2/guide/components.html#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%A7-v-model-%E3%82%92%E4%BD%BF%E3%81%86)

[^1]: Vue 3 ではコンポーネントで使用する v-model の糖衣構文が変更になりました。詳しくは[公式ドキュメント](https://v3.vuejs.org/guide/component-basics.html#using-v-model-on-components)ををご参照ください。