独自コンポーネントでの v-model

双方向データバインディングで紹介したように、v-modelはフォーム入力欄とVueのデータを自動で同期してくれる仕組みです。 このv-modelは、独自のコンポーネントでも同じように使うことができます。

<!-- 親コンポーネント -->
<script setup>
const count = ref(10)
</script>

<template>
  <ClearButton v-model="count" />
</template>

どうやってつながっているの?

v-modelは、親と子の間で「親から子へ値を渡し、子で変更があれば親に返す」というやり取りをできるようにしてくれる仕組みです。 具体的には次の動きができるようになります。

  • 親から子へ、変更可能な値をpropsで渡します(props名は自動的にmodelValueになります)
  • 子で値を変更するために、emit を使って親に通知します(イベント名は自動でupdate:modelValueになります)

こうすることで、親と子のデータが常に同期されるようになります。


この仕組みを子コンポーネントで使うときは、propsemitsを明示的に定義します。

<script setup>
const props = defineProps(['modelValue']) // 親から値を受け取る
const emit = defineEmits(['update:modelValue']) // 変更を親に通知する
</script>

<template>
  <button @click="emit('update:modelValue', 0)">
    カウントリセット
  </button>
</template>

Vue 3.4以降の新しい書き方:defineModel()

上の方法では、親子間で値を自動的に同期できますが、子コンポーネントの記述が少し複雑です。

Vue 3.4以降では、defineModel()マクロを使うことで、子コンポーネント側でmodelValueを変更するだけで済みます(内部的には従来と同じprops/emitが自動的に使われています) なお、内部的にはmodelValueという名前が使われていますが、変数名は子側で自由に変更できます。

<!-- 親コンポーネント -->
<script setup>
const count = ref(0)
</script>

<template>
  <ClearButton v-model="count" />
</template>
<!-- 子コンポーネント -->
<script setup>
const count = defineModel()
</script>

<template>
  <button @click="count = 0">
    カウントリセット
  </button>
</template>

ポイント

  • propsemitを書かなくてもOK
  • 親と子の値がリアルタイムでつながる

複数の v-model(名前付き v-model)

v-modelは、ひとつの値だけでなく、名前を付けて複数の値を同時に親子でやりとりすることもできます。 複雑なフォームの時にとても便利です。

<!-- 親コンポーネント -->
<template>
  <BookEditor
    v-model:title="bookTitle"
    v-model:author="bookAuthor"
  />
</template>
<!-- 子コンポーネント(difneModel使用) -->
<script setup>
const title = defineModel('title')
const author = defineModel('author')
</script>

<template>
  <input v-model="title" placeholder="タイトル">
  <input v-model="author" placeholder="著者">
</template>
<!-- 子コンポーネント(props, emitをを明示的に定義) -->
<script setup>
const props = defineProps([
  'title',
  'author'
])
const emit = defineEmits([
  'update:title',
  'update:author'
])
</script>

<template>
  <input
    :value="props.title"
    @input="emit('update:title', $event.target.value)"
  >
  <input
    :value="props.author"
    @input="emit('update:author', $event.target.value)"
  >
</template>

ポイント

  • 親は v-model:名前 でいくつでも決められる
  • 子は defineModel('名前') で対応できる
  • Vue 3.4未満なら、props/emitsを一つずつ定義する必要あり

まとめ

  • v-modelpropsで値を渡し、emitで変更を返す 仕組みをまとめた構文
  • Vue 3.4以降は defineModel()で記述がさらに簡単
  • 複数の値や名前付きも、同じ構文でシンプルに扱える
  • 従来のフォーム要素と同じ感覚で、独自コンポーネントでも使える

チャレンジ

新規 TODO を追加するモーダルコンポーネント(CreateModal.vue)を作成しましょう。 app.vue では isCreateModalOpen という状態を用意していて、「新規作成」ボタンを押すとモーダルが開くようになっています。 ここでは、モーダルの「閉じる」ボタンでモーダルを閉じられるようにするのが目標です。

  1. 子コンポーネントで defineModel() を使って、modelValue(または任意の名前)を定義する
  2. v-if を使って、その値が true のときだけモーダルを表示するようにする
  3. モーダルの「閉じる」ボタンをクリックすると、この値が false になるようにする
  4. 親コンポーネントから v-modelisCreateModalOpen を子に渡して動作を確認する

もし行き詰まったら、以下のボタンをクリックして解答を見ることができます。

親・子コンポーネント間で isCreateModalOpen を双方向に同期できるようになりました!

リアクティビティー パート2
リアクティビティー パート1で、データの変更を監視して、変更された時に更新を自動的にトリガーする 優れたリアクティビティシステム のrefについて学習しました。 ここでは、computedについて学習しましょう。
独自コンポーネントでの v-slot
v-slot は、親コンポーネントから子コンポーネントの特定の場所にテンプレートを差し込むための仕組みです。 コンポーネントを「箱」として使い、その中に好きなHTMLや部品を入れることができます。
ファイル
エディタ
WebContainerを初期化中
ファイルをマウント中
依存関係をインストール中
Nuxtサーバーを起動中
Nuxtが準備完了を待機中
ターミナル