Introduction
In modern Vue 3 applications, creating reusable dialog components is a common requirement. This article will explore how to build a robust dialog component using Vue 3's composition API, with a focus on v-model implementation and component encapsulation.
Core Implementation
1. Component Types
First, let's define our TypeScript interfaces:
// types/dialog.ts
export interface CategoryForm {
categoryName: string
}
export interface CategoryDialogProps {
visible: boolean
formLabelWidth?: string
loading?: boolean
title?: string
}
export interface CategoryEmits {
(e: 'update:visible', value: boolean): void
(e: 'addCategory', name: string): void
(e: 'close'): void
}
2. Complete Dialog Component
<!-- components/CategoryDialog.vue -->
<template>
<el-dialog
v-model="dialogVisible"
:title="title"
:width="width"
destroy-on-close
center
@close="handleClose"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
:validate-on-rule-change="false"
>
<el-form-item
:label="label"
:label-width="formLabelWidth"
prop="categoryName"
>
<el-input
v-model.trim="form.categoryName"
:placeholder="placeholder"
maxlength="50"
show-word-limit
@keyup.enter="handleConfirm"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button
@click="handleClose"
:disabled="loading"
>
{{ cancelText }}
</el-button>
<el-button
type="primary"
@click="handleConfirm"
:loading="loading"
>
{{ confirmText }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import type { CategoryForm, CategoryDialogProps } from '@/types/dialog'
// Props definition with default values
const props = withDefaults(defineProps<CategoryDialogProps>(), {
visible: false,
formLabelWidth: '80px',
loading: false,
title: 'Add Category',
width: '400px',
label: 'Category Name',
placeholder: 'Please enter category name',
cancelText: 'Cancel',
confirmText: 'Confirm'
})
// Emits definition
const emits = defineEmits<CategoryEmits>()
// Form ref for validation
const formRef = ref<FormInstance>()
// Form data
const form = ref<CategoryForm>({
categoryName: ''
})
// Validation rules
const rules: FormRules = {
categoryName: [
{
required: true,
message: 'Please enter category name',
trigger: 'blur'
},
{
min: 2,
max: 50,
message: 'Length should be 2 to 50 characters',
trigger: 'blur'
},
{
pattern: /^[\w\-\u4e00-\u9fa5]+$/,
message: 'Only letters, numbers, Chinese characters, underscores and hyphens are allowed',
trigger: 'blur'
}
]
}
// Dialog visibility computed property
const dialogVisible = computed({
get() {
return props.visible
},
set(value: boolean) {
emits('update:visible', value)
}
})
// Close handler
const handleClose = () => {
if (props.loading) return
dialogVisible.value = false
resetForm()
emits('close')
}
// Reset form
const resetForm = () => {
form.value.categoryName = ''
formRef.value?.resetFields()
}
// Confirm handler
const handleConfirm = async () => {
if (!formRef.value || props.loading) return
try {
const valid = await formRef.value.validate()
if (valid) {
emits('addCategory', form.value.categoryName)
}
} catch (error) {
console.error('Validation failed:', error)
}
}
// Expose methods
defineExpose({
resetForm,
validate: () => formRef.value?.validate()
})
</script>
<style scoped>
.dialog-footer {
padding-top: 20px;
text-align: right;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
:deep(.el-input__wrapper) {
width: 100%;
}
</style>
3. Usage Example
<!-- views/CategoryManagement.vue -->
<template>
<div class="category-management">
<div class="header">
<h2>Category Management</h2>
<el-button
type="primary"
@click="openDialog"
>
Add Category
</el-button>
</div>
<el-table
:data="categories"
border
style="width: 100%"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="createTime" label="Create Time" width="180" />
<el-table-column label="Actions" width="150" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
link
@click="handleEdit(row)"
>
Edit
</el-button>
<el-button
type="danger"
link
@click="handleDelete(row)"
>
Delete
</el-button>
</template>
</el-table-column>
</el-table>
<CategoryDialog
v-model:visible="dialogVisible"
:loading="loading"
@addCategory="handleAddCategory"
@close="handleDialogClose"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import CategoryDialog from '@/components/CategoryDialog.vue'
// State
const dialogVisible = ref(false)
const loading = ref(false)
const categories = ref<Category[]>([])
// Open dialog
const openDialog = () => {
dialogVisible.value = true
}
// Add category
const handleAddCategory = async (name: string) => {
try {
loading.value = true
// API call would go here
await new Promise(resolve => setTimeout(resolve, 1000))
categories.value.push({
id: Date.now(),
name,
createTime: new Date().toLocaleString()
})
ElMessage.success('Category added successfully')
dialogVisible.value = false
} catch (error) {
ElMessage.error('Failed to add category')
} finally {
loading.value = false
}
}
// Dialog close
const handleDialogClose = () => {
// Additional cleanup if needed
}
</script>
<style scoped>
.category-management {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h2 {
margin: 0;
font-size: 20px;
font-weight: 500;
}
</style>
Key Features
1. v-model Implementation
The core of our dialog component's two-way binding is implemented through a computed property:
const dialogVisible = computed({
get() {
return props.visible
},
set(value: boolean) {
emits('update:visible', value)
}
})
2. Form Validation
We implement comprehensive form validation with async support:
const handleConfirm = async () => {
if (!formRef.value || props.loading) return
try {
const valid = await formRef.value.validate()
if (valid) {
emits('addCategory', form.value.categoryName)
}
} catch (error) {
console.error('Validation failed:', error)
}
}
3. Loading State
The component handles loading states gracefully:
const handleClose = () => {
if (props.loading) return
dialogVisible.value = false
resetForm()
emits('close')
}
Best Practices
-
TypeScript Integration
- Use proper type definitions for props, emits, and refs
- Leverage TypeScript's type inference where possible
-
Component Design
- Follow Single Responsibility Principle
- Implement proper prop validation
- Handle edge cases and error states
-
Performance Optimization
- Use
v-show
for frequently toggled elements - Implement proper cleanup on component destruction
- Avoid unnecessary re-renders
- Use
-
User Experience
- Add loading states
- Implement keyboard navigation
- Provide clear feedback for user actions
Common Pitfalls and Solutions
1. Form Reset Issues
Always reset the form state when closing the dialog:
const resetForm = () => {
form.value.categoryName = ''
formRef.value?.resetFields()
}
2. Validation Timing
Ensure validation happens at the right time:
<el-form
:validate-on-rule-change="false"
@submit.prevent
>
3. Loading State Management
Properly handle loading states to prevent multiple submissions:
if (props.loading) return
Conclusion
Building a robust dialog component in Vue 3 requires careful attention to various aspects:
- Proper v-model implementation
- Form validation
- Loading states
- TypeScript integration
- Error handling
By following these patterns and best practices, you can create reusable, maintainable dialog components that provide a great user experience.
Resources
Remember that this implementation can be further customized based on your specific needs. The key is maintaining a balance between functionality, reusability, and maintainability.