字节笔记本
2026年7月19日
Vue 3 v-model and Dialog Component Encapsulation
Vue 3's v-model is syntactic sugar for a prop plus an emit. On a standard <input>, it binds value and listens for input (or @update:modelValue in Vue 3). On custom components, it does the same thing but with modelValue and update:modelValue. Understanding this expansion is the key to using v-model correctly on your own components — especially Dialogs, where the visible state needs to be controlled from outside but toggled from inside.
How v-model Expands
<!-- This -->
<ChildComponent v-model="isVisible" />
<!-- Expands to this -->
<ChildComponent
:modelValue="isVisible"
@update:modelValue="newValue => isVisible = newValue"
/>Custom v-model Names
You can name the v-model binding to avoid ambiguity when a component has multiple two-way bindings:
<ChildComponent v-model:visible="isVisible" />This expands to :visible + @update:visible. The prop name matches whatever comes after v-model:.
Implementing a Dialog with v-model:visible
Here's a complete Dialog component using Element Plus, but the pattern works with any UI library:
<template>
<el-dialog
v-model="dialogVisible"
title="Add Category"
width="400px"
destroy-on-close
center
@close="closeDialog"
>
<el-form :model="form" :rules="rules" ref="formRef">
<el-form-item label="Category Name" :label-width="formLabelWidth" prop="categoryName">
<el-input v-model="form.categoryName" placeholder="Enter category name" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeDialog">Cancel</el-button>
<el-button type="primary" @click="confirmAdd">Confirm</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { FormInstance } from 'element-plus'
const props = defineProps({
visible: { type: Boolean, required: true },
formLabelWidth: { type: String, default: '80px' }
})
const emits = defineEmits(['update:visible', 'addCategory'])
// Computed property bridges prop → emit
const dialogVisible = computed({
get() {
return props.visible
},
set(value) {
emits('update:visible', value)
}
})
const formRef = ref<FormInstance>()
const form = ref({ categoryName: '' })
const rules = {
categoryName: [
{ required: true, message: 'Please enter category name', trigger: 'blur' },
{ min: 2, max: 50, message: 'Length should be 2 to 50 characters', trigger: 'blur' }
]
}
const closeDialog = () => {
dialogVisible.value = false
form.value.categoryName = ''
formRef.value?.resetFields()
}
const confirmAdd = () => {
if (!formRef.value) return
formRef.value.validate((valid: boolean) => {
if (valid) {
emits('addCategory', form.value.categoryName.trim())
closeDialog()
}
})
}
</script>The computed property is doing the real work here. Its getter reads from the prop, and its setter emits back to the parent. This is the standard pattern for v-model on custom components.
TypeScript Types for Props and Emits
interface Props {
visible: boolean
formLabelWidth?: string
}
interface Emits {
(e: 'update:visible', value: boolean): void
(e: 'addCategory', name: string): void
}
const props = withDefaults(defineProps<Props>(), {
formLabelWidth: '80px'
})
const emits = defineEmits<Emits>()Usage in Parent Component
<template>
<div>
<el-button @click="dialogVisible = true">Open Dialog</el-button>
<CategoryDialog
v-model:visible="dialogVisible"
@addCategory="handleAdd"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const dialogVisible = ref(false)
const handleAdd = (category: string) => {
console.log('Added:', category)
}
</script>Multiple v-models
Vue 3 lets you bind multiple v-models on the same component:
<UserDialog
v-model:visible="isVisible"
v-model:username="username"
v-model:age="age"
/>Each one gets its own computed property inside the child, following the same getter/setter pattern.
Common Mistakes
- Mutating the prop directly (
props.visible = false) — Vue will warn you, and it breaks reactivity. Always emit the update. - Forgetting
destroy-on-close— without it, the form keeps stale data when the dialog reopens. - Not resetting form state on close — always clear form fields and call
resetFields()when closing.