按照前一天的綁定模式做一個「型表單範例」,直接看到 v-model
底層從「共享 → 包裝 → 轉換」的效果,今天透過更詳細的案例說明,不只知道 v-model
怎麼用,還能理解「為什麼這樣設計、它的內在機制」。
v-model
的內部契約:
defineModel()
(或傳統的 props + emit
)就能支援父層的雙向綁定。.trim
, .capitalize
, .uppercase
...) 透過 get/set
自訂資料流轉邏輯。UserName.vue
同時管理 firstName
和 lastName
,實際感受到「一個父層 state → 多個子元件」的共享。DeepChild.vue
用 inject
直接拿到全域的 form
,展現 Vue3.5 v-model
在跨層結合 provide/inject 時的威力。App.vue
的 form
結構或修飾符,測試「父層資料流」與「子元件轉換」的關係。從父層 form
變更 → v-model
傳遞 → 子元件處理 → 回寫父層,證明 Vue3.5 v-model
的「單一真相來源」設計。
App.vue (form) ─── v-model → Wrapper.vue ─── v-model → BaseInput.vue
DeepChild.vue: inject(form)
↳ 直接拿到父層 provide 的同一份 form 物件
為了「可直接在瀏覽器執行」所以請 cursor 把以下專案範例轉成可以在 codepen 示範
v-model
的契約(modelValue
與update:modelValue
,以及xxxModifiers
)的寫法,這樣方便理解也不用開新專案 run。
codepen - Vue 3.5 v-model 表單範例
<script setup lang="ts">
const [model, modifiers] = defineModel<string>({
set(v) {
// 支援 .trim 修飾符
return modifiers.trim && typeof v === 'string' ? v.trim() : v
}
})
defineProps<{ placeholder?: string }>()
</script>
<template>
<input class="input" :placeholder="placeholder" v-model="model" />
</template>
<script setup lang="ts">
const [model, modifiers] = defineModel<string>({
set(v) {
return modifiers.trim && typeof v === 'string' ? v.trim() : v
}
})
</script>
<template>
<div class="stack">
<label class="label"><slot name="label" />(Wrapper)</label>
<BaseInput v-model="model" />
<div class="hint"><slot name="hint" /></div>
</div>
</template>
v-model
+ 修飾符)<script setup lang="ts">
const [first, firstMods] = defineModel<string>('firstName', {
set(v) {
if (firstMods.capitalize && typeof v === 'string') {
return v.charAt(0).toUpperCase() + v.slice(1)
}
return v
}
})
const [last, lastMods] = defineModel<string>('lastName', {
set(v) {
if (lastMods.uppercase && typeof v === 'string') {
return v.toUpperCase()
}
return v
}
})
</script>
<template>
<div class="row">
<input class="input" placeholder="First name" v-model="first" />
<input class="input" placeholder="Last name" v-model="last" />
</div>
</template>
inject
共用)<script setup lang="ts">
const form = inject<{ email: string }>('form')!
</script>
<template>
<input class="input" placeholder="user@example.com" v-model="form.email" />
</template>
<script setup lang="ts">
import { reactive, provide, computed } from 'vue'
import BaseInput from './BaseInput.vue'
import FieldWrapper from './FieldWrapper.vue'
import UserName from './UserName.vue'
import EmailEditor from './EmailEditor.vue'
const form = reactive({
nativeNote: '',
searchText: '',
nameDisplay: '',
person: { first: '', last: '' },
email: ''
})
// 提供給深層子孫
provide('form', form)
const pretty = computed(() => JSON.stringify(form, null, 2))
</script>
<template>
<section>
<!-- 1) 原生 input 的 v-model -->
<h2>① 原生 input</h2>
<input v-model="form.nativeNote" placeholder="打點字…" />
<pre>{{ form.nativeNote }}</pre>
<!-- 2) 自訂元件:單一 v-model + 修飾符支援(.trim) -->
<h2>② BaseInput(支援 .trim)</h2>
<BaseInput v-model.trim="form.searchText" placeholder="搜尋…" />
<pre>{{ form.searchText }}</pre>
<!-- 3) 包裝元件(Wrapper):同一份狀態跨層傳遞,把同一個 model 往內層再綁一次(轉接/驗證/美化) -->
<h2>③ Wrapper → BaseInput</h2>
<FieldWrapper v-model.trim="form.nameDisplay">
<template #label>顯示名稱</template>
<template #hint>Wrapper 再把同一個 model 傳進 BaseInput</template>
</FieldWrapper>
<pre>{{ form.nameDisplay }}</pre>
<!-- 4) 多模型:同一個元件同時管理多個 v-model 值 + 修飾符 -->
<h2>④ 多模型</h2>
<UserName
v-model:first-name.capitalize="form.person.first"
v-model:last-name.uppercase="form.person.last"
/>
<pre>{{ form.person }}</pre>
<!-- 5) provide/inject:深層子孫直接用同一份 reactive 狀態(跨元件共享) -->
<h2>⑤ provide/inject</h2>
<EmailEditor />
<pre>{{ form.email }}</pre>
<h3>整份 form:</h3>
<pre>{{ pretty }}</pre>
</section>
</template>