ByteNoteByteNote

字节笔记本

2026年7月19日

Vue 3 v-model and Dialog Component Encapsulation

API中转
¥120

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

vue
<!-- 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:

vue
<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:

vue
<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

typescript
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

vue
<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:

vue
<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.
分享: