25
Help us understand the problem. What are the problem?

posted at

updated at

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

はじめに

:warning: 本稿は Vue 2.x が対象になります。Vue 3 に関する記事はこちらを参照ください。

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

サンプルコード

▼ v-model を利用しないサンプルコード
Edit 【Vue.js 再入門】v-model を正しく理解して親子コンポーネント間のデータ伝播をマスターする

▼ v-model を利用したサンプルコード
Edit 【Vue.js 再入門】v-model を正しく理解して親子コンポーネント間のデータ伝播をマスターする(v-model ver.)

御復習い(おさらい)

v-model は双方向データバインディングを実現するディレクティブです。双方向データバインディングとは、ビューで変更があった場合、その値を Vue インスタンスのデータとして更新します。逆に Vue インスタンスのデータに変更があった場合はビューを再レンダリングします。

<input v-model="message" />

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

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

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

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

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

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

本題

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

テキスト入力欄

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

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

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

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

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

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

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

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

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

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

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

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

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

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

子コンポーネント
<!-- 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 属性値を親に伝搬します。

子コンポーネント
<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 に代入します。

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

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

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

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

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

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

子コンポーネント
<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 に限らず正しく仕様を理解することは重要です。本稿がどなたかの一助になれば幸甚です。

参考文献


  1. Vue 3 ではコンポーネントで使用する v-model の糖衣構文が変更になりました。詳しくは公式ドキュメントををご参照ください。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
25
Help us understand the problem. What are the problem?