iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Vue.js

Vue3.6 的革新:深入理解 Composition API系列 第 13

Day 13: 瞭解 v-model 的實現細節與案例

  • 分享至 

  • xImage
  •  

按照前一天的綁定模式做一個「型表單範例」,直接看到 v-model 底層從「共享 → 包裝 → 轉換」的效果,今天透過更詳細的案例說明,不只知道 v-model 怎麼用,還能理解「為什麼這樣設計、它的內在機制」。

知識點大集合


  1. 看到 v-model 的內部契約
    • 子元件只要 defineModel()(或傳統的 props + emit)就能支援父層的雙向綁定。
    • 修飾符 (.trim, .capitalize, .uppercase...) 透過 get/set 自訂資料流轉邏輯。
  2. 體驗多層級/跨元件的資料傳遞
    • UserName.vue 同時管理 firstNamelastName,實際感受到「一個父層 state → 多個子元件」的共享。
    • DeepChild.vueinject 直接拿到全域的 form,展現 Vue3.5 v-model 在跨層結合 provide/inject 時的威力。
  3. 保留可執行的學習案例
    • 修改 App.vueform 結構或修飾符,測試「父層資料流」與「子元件轉換」的關係。

案例


從父層 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 的契約(modelValueupdate:modelValue,以及 xxxModifiers)的寫法,這樣方便理解也不用開新專案 run。
codepen - Vue 3.5 v-model 表單範例

BaseInput.vue

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

FieldWrapper.vue (包裝元件 → 轉接同一份 model)

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

UserName.vue (多個 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>

EmailEditor.vue(深層 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>

App.vue(根組件)

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

上一篇
Day 12: v-model 的進化
系列文
Vue3.6 的革新:深入理解 Composition API13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言