API 風格偏好

組件 v-model

v-model 可以在組件上使用以實現雙向綁定。

首先讓我們回憶一下 v-model 在原生元素上的用法:

<input v-model="searchText" />

在代碼背後,模板編譯器會對 v-model 進行更冗長的等價展開。因此上面的代碼其實等價於下面這段:

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

而當使用在一個組件上時,v-model 會被展開為如下的形式:

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

要讓這個例子實際工作起來,<CustomInput> 組件內部需要做兩件事:

  1. 將內部原生 <input> 元素的 value attribute 綁定到 modelValue prop
  2. 當原生的 input 事件觸發時,觸發一個攜帶了新值的 update:modelValue 自定義事件

這里是相應的代碼:

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

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

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

現在 v-model 可以在這個組件上正常工作了:

<CustomInput v-model="searchText" />

另一種在組件內實現 v-model 的方式是使用一個可寫的,同時具有 getter 和 setter 的 computed 屬性。get 方法需返回 modelValue prop,而 set 方法需觸發相應的事件:

<!-- 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>
<!-- CustomInput.vue -->
<script setup>
import { computed } from 'vue'

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

const value = computed({
  get() {
    return props.modelValue
  },
  set(value) {
    emit('update:modelValue', value)
  }
})
</script>

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

v-model 的參數

默認情況下,v-model 在組件上都是使用 modelValue 作為 prop,並以 update:modelValue 作為對應的事件。我們可以通過給 v-model 指定一個參數來更改這些名字:

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

在這個例子中,子組件應聲明一個 title prop,並通過觸發 update:title 事件更新父組件值:

<!-- MyComponent.vue -->
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>

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

在演練場中嘗試一下

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

在演練場中嘗試一下

多個 v-model 綁定

利用剛才在 v-model 參數小節中學到的指定參數與事件名的技巧,我們可以在單個組件實例上創建多個 v-model 雙向綁定。

組件上的每一個 v-model 都會同步不同的 prop,而無需額外的選項:

<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>
<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>

在演練場中嘗試一下

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

在演練場中嘗試一下

處理 v-model 修飾符

在學習輸入綁定時,我們知道了 v-model 有一些內置的修飾符,例如 .trim.number.lazy。在某些場景下,你可能想要一個自定義組件的 v-model 支持自定義的修飾符。

我們來創建一個自定義的修飾符 capitalize,它會自動將 v-model 綁定輸入的字符串值第一個字母轉為大寫:

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

組件的 v-model 上所添加的修飾符,可以通過 modelModifiers prop 在組件內訪問到。在下面的組件中,我們聲明了 modelModifiers 這個 prop,它的默認值是一個空對象:

<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

defineEmits(['update:modelValue'])

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

<template>
  <input
    type="text"
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>
<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 prop 包含了 capitalize 且其值為 true,因為它在模板中的 v-model 綁定 v-model.capitalize="myText" 上被使用了。

有了這個 prop,我們就可以檢查 modelModifiers 對象的鍵,並編寫一個處理函數來改變拋出的值。在下面的代碼里,我們就是在每次 <input /> 元素觸發 input 事件時將值的首字母大寫:

<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="modelValue" @input="emitValue" />
</template>

在演練場中嘗試一下

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

在演練場中嘗試一下

帶參數的 v-model 修飾符

對於又有參數又有修飾符的 v-model 綁定,生成的 prop 名將是 arg + "Modifiers"。舉例來說:

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

相應的聲明應該是:

const props = defineProps(['title', 'titleModifiers'])
defineEmits(['update:title'])

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

這里是另一個例子,展示了如何在使用多個不同參數的 v-model 時使用修飾符:

<UserName
  v-model:first-name.capitalize="first"
  v-model:last-name.uppercase="last"
/>
<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>
<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>