コンポーネントの v-model
基本的な使い方
コンポーネント上で v-model
を使用すると双方向バインディングを実装できます。
Vue 3.4 以降は、defineModel()
マクロを使うことが推奨されています:
vue
<!-- Child.vue -->
<script setup>
const model = defineModel()
function update() {
model.value++
}
</script>
<template>
<div>Parent bound v-model is: {{ model }}</div>
<button @click="update">Increment</button>
</template>
親は v-model
で値をバインドできます:
template
<!-- Parent.vue -->
<Child v-model="countModel" />
defineModel()
が返す値は ref です。他の ref と同じようにアクセスしたり変更したりできますが、親の値とローカルの値の双方向バインディングとして動作する点が異なります:
- その
.value
は親のv-model
にバインドされた値と同期される。 - 子が
.value
を変更すると、親にバインドされている値も更新される。
つまり、v-model
を使ってネイティブの入力要素にこの ref をバインドすることもでき、同じ v-model
の使い方を提供しながら、ネイティブの入力要素をラップするのが簡単になります:
vue
<script setup>
const model = defineModel()
</script>
<template>
<input v-model="model" />
</template>
内部の仕組み
defineModel
は便利なマクロです。コンパイラーはこれを次のように展開します:
modelValue
という名前の props: ローカル ref の値が同期されます。update:modelValue
という名前のイベント: ローカル ref の値が変更された時に発行されます。
3.4 以前は、上記の子コンポーネントはこのように実装されていました:
vue
<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
すると、親コンポーネントの v-model="foo"
は次のようにコンパイルされます:
template
<!-- Parent.vue -->
<Child
:modelValue="foo"
@update:modelValue="$event => (foo = $event)"
/>
見ての通り、かなり冗長です。ただ、内部で何が起こっているのかを理解するのに役立ちます。
defineModel
は props を宣言するので、元となる props のオプションを defineModel
に渡して宣言できます:
js
// v-model を必須にする
const model = defineModel({ required: true })
// デフォルト値を提供する
const model = defineModel({ default: 0 })
WARNING
もし defineModel
props に default
値を指定し、親コンポーネントからこの props に何も値を与えなかった場合、親と子のコンポーネント間で同期が取れなくなる可能性があります。以下の例では、親コンポーネントの myRef
は undefined ですが、子コンポーネントの model
は 1 です:
js
// 子コンポーネント:
const model = defineModel({ default: 1 })
// 親コンポーネント:
const myRef = ref()
html
<Child v-model="myRef"></Child>
v-model
の引数
コンポーネントの v-model
にも引数を指定できます:
template
<MyComponent v-model:title="bookTitle" />
子コンポーネントでは、defineModel()
の第一引数に文字列を渡すことで、対応する引数をサポートできます:
vue
<!-- MyComponent.vue -->
<script setup>
const title = defineModel('title')
</script>
<template>
<input type="text" v-model="title" />
</template>
props のオプションも必要な場合は、モデル名の後に渡します:
js
const title = defineModel('title', { required: true })
3.4 以前の使用法
vue
<!-- MyComponent.vue -->
<script setup>
defineProps({
title: {
required: true
}
})
defineEmits(['update:title'])
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
複数の v-model
のバインディング
先ほど v-model
の引数で学んだように、特定の props とイベントをターゲットにする機能を活用することで、1 つのコンポーネントインスタンスに複数の v-model
バインディングを作成できるようになりました。
各 v-model
は、コンポーネントで追加のオプションを必要とせずに、別の props に同期します:
template
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
vue
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>
<template>
<input type="text" v-model="firstName" />
<input type="text" v-model="lastName" />
</template>
3.4 以前の使用法
vue
<script setup>
defineProps({
firstName: String,
lastName: String
})
defineEmits(['update:firstName', 'update:lastName'])
</script>
<template>
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
/>
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
/>
</template>
v-model
修飾子の処理
フォームの入力バインディングについて学習しているときに、v-model
には 組み込みの修飾子(.trim
, .number
, .lazy
)があることを確認しました。場合によっては、カスタム入力コンポーネントの v-model
でカスタム修飾子をサポートしたいかもしれません。
カスタム修飾子の例として、v-model
バインディングによって提供される文字列の最初の文字を大文字にする capitalize
を作成してみましょう:
template
<MyComponent v-model.capitalize="myText" />
コンポーネントの v-model
に追加された修飾子は、defineModel()
の戻り値を次のように分割代入することで、子コンポーネント内でアクセスできます:
vue
<script setup>
const [model, modifiers] = defineModel()
console.log(modifiers) // { capitalize: true }
</script>
<template>
<input type="text" v-model="model" />
</template>
修飾子に基づいて値の読み書きを条件付きで調整するために、defineModel()
に get
と set
オプションを渡すことができます。これら 2 つのオプションは、モデルの ref の読み取り・設定時に値を受け取り、変換された値を返す必要があります。以下は set
オプションを使って capitalize
修飾子を実装する方法です:
vue
<script setup>
const [model, modifiers] = defineModel({
set(value) {
if (modifiers.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
return value
}
})
</script>
<template>
<input type="text" v-model="model" />
</template>
3.4 以前の使用法
vue
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
const emit = defineEmits(['update:modelValue'])
function emitValue(e) {
let value = e.target.value
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
<template>
<input type="text" :value="props.modelValue" @input="emitValue" />
</template>
引数を持つ v-model
の修飾子
以下は異なる引数を持つ複数の v-model
で修飾子を使用するもう 1 つの例です:
template
<UserName
v-model:first-name.capitalize="first"
v-model:last-name.uppercase="last"
/>
vue
<script setup>
const [firstName, firstNameModifiers] = defineModel('firstName')
const [lastName, lastNameModifiers] = defineModel('lastName')
console.log(firstNameModifiers) // { capitalize: true }
console.log(lastNameModifiers) // { uppercase: true }
</script>
3.4 以前の使用法
vue
<script setup>
const props = defineProps({
firstName: String,
lastName: String,
firstNameModifiers: { default: () => ({}) },
lastNameModifiers: { default: () => ({}) }
})
defineEmits(['update:firstName', 'update:lastName'])
console.log(props.firstNameModifiers) // { capitalize: true }
console.log(props.lastNameModifiers) // { uppercase: true }
</script>