Understanding v-model in Vue 3
The Essence of v-model
In Vue 3, v-model is essentially syntactic sugar that expands into two parts:
- A
modelValue
prop - An
update:modelValue
event
<!-- Using v-model -->
<ChildComponent v-model="searchText" />
<!-- Equivalent to -->
<ChildComponent
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
Custom v-model Names
Vue 3 allows customizing v-model names for better clarity and multiple bindings:
<!-- Parent component -->
<ChildComponent v-model:visible="isVisible" />
<!-- Equivalent to -->
<ChildComponent
:visible="isVisible"
@update:visible="newValue => isVisible = newValue"
/>
Multiple v-models
Vue 3 supports multiple v-models on a single component:
<UserDialog
v-model:visible="isVisible"
v-model:username="username"
v-model:age="age"
/>
Implementing a Reusable Dialog Component
Complete Dialog Component Implementation
<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="Please 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="confirmAddCategory">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'])
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 confirmAddCategory = () => {
if (!formRef.value) return
formRef.value.validate((valid: boolean) => {
if (valid) {
emits('addCategory', form.value.categoryName.trim())
closeDialog()
}
})
}
</script>
Key Concepts Explained
Two-way Binding with Computed Properties
// Basic version
const dialogVisible = computed({
get() {
return props.visible
},
set(value) {
emits('update:visible', value)
}
})
// Complete version with TypeScript
const dialogVisible = computed<boolean>({
get() {
return props.visible
},
set(value: boolean) {
emits('update:visible', value)
}
})
TypeScript Integration
// Props type definition
interface Props {
visible: boolean
modelValue?: string
}
// Emits type definition
interface Emits {
(e: 'update:visible', value: boolean): void
(e: 'update:modelValue', value: string): void
(e: 'close'): void
}
// Implementation
const props = withDefaults(defineProps<Props>(), {
modelValue: ''
})
const emits = defineEmits<Emits>()
Best Practices
Default Props Configuration
const props = withDefaults(defineProps<Props>(), {
visible: false,
modelValue: '',
width: '400px'
})
State Reset Handling
const resetState = () => {
// Reset form
formRef.value?.resetFields()
// Reset local state
form.value = {
categoryName: ''
}
// Notify parent
emits('update:modelValue', '')
}
const handleClose = () => {
resetState()
emits('close')
}
Component Methods Exposure
const open = () => {
dialogVisible.value = true
}
const close = () => {
dialogVisible.value = false
}
defineExpose({
open,
close
})
Usage Example
<template>
<div>
<el-button @click="openDialog">Open Dialog</el-button>
<CategoryDialog
v-model:visible="dialogVisible"
@addCategory="handleAddCategory"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const dialogVisible = ref(false)
const openDialog = () => {
dialogVisible.value = true
}
const handleAddCategory = (category: string) => {
console.log('New category:', category)
dialogVisible.value = false
}
</script>
Tips and Common Pitfalls
-
Always use computed properties for v-model implementations to ensure proper reactivity
-
Reset state properly when closing the dialog to prevent stale data
-
Type your emits and props for better development experience and error catching
-
Use meaningful v-model names when implementing multiple v-models
-
Consider component lifecycle when handling state resets and cleanup
-
Implement proper validation before emitting events to the parent component
Conclusion
Vue 3's v-model implementation provides a flexible and powerful way to handle two-way data binding in components. When combined with TypeScript and proper component design patterns, it enables the creation of robust and reusable components like dialogs. Remember to follow the best practices outlined above to ensure your components are maintainable and type-safe.