跳轉到內容

元件 v-model

基本用法

v-model 可以用於元件上以實現雙向繫結。

從 Vue 3.4 開始,推薦的做法是使用 defineModel()

vue
<!-- Child.vue -->
<script setup>
const model = defineModel()

function update() {
  model.value++
}
</script>

<template>
  <div>Parent bound v-model is: {{ model }}</div>
  <button @click="update">Increment</button>
</template>

父元件可以使用 v-model 繫結一個值

template
<!-- Parent.vue -->
<Child v-model="countModel" />

defineModel() 返回的值是一個 ref。它可以像任何其他 ref 一樣被訪問和修改,不同之處在於它充當父值和區域性值之間的雙向繫結

  • 它的 .value 與父元件繫結的 v-model 值同步;
  • 當它被子元件修改時,也會更新父元件繫結的值。

這意味著您還可以使用 v-model 將此 ref 繫結到原生輸入元素,使包裝原生輸入元素同時提供相同的 v-model 使用方式變得簡單

vue
<script setup>
const model = defineModel()
</script>

<template>
  <input v-model="model" />
</template>

在 playground 中嘗試

內部原理

defineModel 是一個便利宏。編譯器將其擴充套件為以下內容

  • 一個名為 modelValue 的 prop,該 prop 的值與區域性 ref 的值同步;
  • 一個名為 update:modelValue 的事件,當局部 ref 的值被修改時發出。

這是在 3.4 之前實現上述相同子元件的方式

vue
<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

然後,父元件中的 v-model="foo" 將被編譯為

template
<!-- Parent.vue -->
<Child
  :modelValue="foo"
  @update:modelValue="$event => (foo = $event)"
/>

如你所見,這要冗長得多。然而,瞭解底層發生的事情是有幫助的。

因為 defineModel 聲明瞭一個 prop,所以你可以透過將其傳遞給 defineModel 來宣告底層 prop 的選項

js
// making the v-model required
const model = defineModel({ required: true })

// providing a default value
const model = defineModel({ default: 0 })

警告

如果您為 defineModel prop 提供了預設值,並且父元件沒有為該 prop 提供任何值,這可能會導致父元件和子元件之間的解同步。在下面的示例中,父元件的 myRef 是未定義的,但子元件的 model 是 1

js
// child component:
const model = defineModel({ default: 1 })

// parent component:
const myRef = ref()
html
<Child v-model="myRef"></Child>

首先,讓我們回顧一下在原生元素上如何使用 v-model

template
<input v-model="searchText" />

在底層,模板編譯器將 v-model 擴充套件為更冗長的等效程式碼。所以上面的程式碼與以下程式碼相同

template
<input
  :value="searchText"
  @input="searchText = $event.target.value"
/>

在元件上使用時,v-model 將擴充套件為以下內容

template
<CustomInput
  :model-value="searchText"
  @update:model-value="newValue => searchText = newValue"
/>

不過,為了真正實現這個功能,<CustomInput> 元件必須做兩件事

  1. 將原生 <input> 元素的 value 屬性繫結到 modelValue prop
  2. 當原生 input 事件被觸發時,發出一個帶有新值的 update:modelValue 自定義事件

這就是它在實際中的應用

vue
<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

現在 v-model 應該可以與這個元件完美地工作

template
<CustomInput v-model="searchText" />

在 playground 中嘗試

在元件內實現 v-model 的另一種方法是使用具有獲取器和設定器的可寫 computed 屬性。獲取器方法應返回 modelValue 屬性,設定器方法應發出相應的事件

vue
<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
}
</script>

<template>
  <input v-model="value" />
</template>

v-model 引數

元件上的 v-model 也可以接受一個引數

template
<MyComponent v-model:title="bookTitle" />

在子元件中,我們可以透過將一個字串傳遞給 defineModel() 作為其第一個引數來支援相應的引數

vue
<!-- MyComponent.vue -->
<script setup>
const title = defineModel('title')
</script>

<template>
  <input type="text" v-model="title" />
</template>

在 playground 中嘗試

如果還需要 prop 選項,它們應放在模型名稱之後傳遞

js
const title = defineModel('title', { required: true })
3.4 之前的使用方法
vue
<!-- MyComponent.vue -->
<script setup>
defineProps({
  title: {
    required: true
  }
})
defineEmits(['update:title'])
</script>

<template>
  <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
</template>

在 playground 中嘗試

在這種情況下,子元件應期望一個 title prop 併發出一個 update:title 事件來更新父值,而不是預設的 modelValue prop 和 update:modelValue 事件

vue
<!-- MyComponent.vue -->
<script>
export default {
  props: ['title'],
  emits: ['update:title']
}
</script>

<template>
  <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
</template>

在 playground 中嘗試

多個 v-model 繫結

透過利用之前學習過的針對特定屬性和事件進行定位的能力,例如使用 v-model 引數,我們現在可以在單個元件例項上建立多個 v-model 繫結。

每個 v-model 都將同步到不同的屬性,無需在元件中新增額外選項。

template
<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>
vue
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

<template>
  <input type="text" v-model="firstName" />
  <input type="text" v-model="lastName" />
</template>

在 playground 中嘗試

3.4 之前的使用方法
vue
<script setup>
defineProps({
  firstName: String,
  lastName: String
})

defineEmits(['update:firstName', 'update:lastName'])
</script>

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

在 playground 中嘗試

vue
<script>
export default {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['update:firstName', 'update:lastName']
}
</script>

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

在 playground 中嘗試

處理 v-model 修飾符

在我們學習關於表單輸入繫結時,我們看到了 v-model 有內建的修飾符 - .trim.number.lazy。在某些情況下,你可能還希望你的自定義輸入元件的 v-model 支援自定義修飾符。

讓我們建立一個示例自定義修飾符,名為 capitalize,它可以首字母大寫由 v-model 繫結提供的字串。

template
<MyComponent v-model.capitalize="myText" />

新增到元件 v-model 的修飾符可以透過解構 defineModel() 返回值來訪問,如下所示

vue
<script setup>
const [model, modifiers] = defineModel()

console.log(modifiers) // { capitalize: true }
</script>

<template>
  <input type="text" v-model="model" />
</template>

為了根據修飾符有條件地調整讀取/寫入值的方式,我們可以將 getset 選項傳遞給 defineModel()。這兩個選項接收模型引用的 get / set 時的值,並應返回一個轉換後的值。這就是我們如何使用 set 選項來實現 capitalize 修飾符的示例。

vue
<script setup>
const [model, modifiers] = defineModel({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
</script>

<template>
  <input type="text" v-model="model" />
</template>

在 playground 中嘗試

3.4 之前的使用方法
vue
<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

const emit = defineEmits(['update:modelValue'])

function emitValue(e) {
  let value = e.target.value
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:modelValue', value)
}
</script>

<template>
  <input type="text" :value="props.modelValue" @input="emitValue" />
</template>

在 playground 中嘗試

新增到元件 v-model 的修飾符將透過 modelModifiers 屬性提供給元件。在下面的示例中,我們建立了一個包含預設為空物件的 modelModifiers 屬性的元件。

vue
<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  created() {
    console.log(this.modelModifiers) // { capitalize: true }
  }
}
</script>

<template>
  <input
    type="text"
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

注意元件的 modelModifiers 屬性包含 capitalize,其值為 true - 由於它在 v-model 繫結 v-model.capitalize="myText" 上被設定。

現在我們已經設定了屬性,我們可以檢查 modelModifiers 物件的鍵,並編寫一個處理程式來更改發出的值。在下面的程式碼中,我們將首字母大寫字串,每當 <input /> 元素觸發 input 事件時。

vue
<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  methods: {
    emitValue(e) {
      let value = e.target.value
      if (this.modelModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1)
      }
      this.$emit('update:modelValue', value)
    }
  }
}
</script>

<template>
  <input type="text" :value="modelValue" @input="emitValue" />
</template>

在 playground 中嘗試

v-model 具有引數的修飾符

對於同時具有引數和修飾符的 v-model 繫結,生成的屬性名將是 arg + "Modifiers"。例如

template
<MyComponent v-model:title.capitalize="myText">

相應的宣告應該是

js
export default {
  props: ['title', 'titleModifiers'],
  emits: ['update:title'],
  created() {
    console.log(this.titleModifiers) // { capitalize: true }
  }
}

下面是使用多個 v-model 和不同引數修飾符的另一個示例

template
<UserName
  v-model:first-name.capitalize="first"
  v-model:last-name.uppercase="last"
/>
vue
<script setup>
const [firstName, firstNameModifiers] = defineModel('firstName')
const [lastName, lastNameModifiers] = defineModel('lastName')

console.log(firstNameModifiers) // { capitalize: true }
console.log(lastNameModifiers) // { uppercase: true }
</script>
3.4 之前的使用方法
vue
<script setup>
const props = defineProps({
firstName: String,
lastName: String,
firstNameModifiers: { default: () => ({}) },
lastNameModifiers: { default: () => ({}) }
})
defineEmits(['update:firstName', 'update:lastName'])

console.log(props.firstNameModifiers) // { capitalize: true }
console.log(props.lastNameModifiers) // { uppercase: true }
</script>
vue
<script>
export default {
  props: {
    firstName: String,
    lastName: String,
    firstNameModifiers: {
      default: () => ({})
    },
    lastNameModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:firstName', 'update:lastName'],
  created() {
    console.log(this.firstNameModifiers) // { capitalize: true }
    console.log(this.lastNameModifiers) // { uppercase: true }
  }
}
</script>
元件 v-model 已載入