Przejdź bezpośrednio do treści

v-model w komponentach

Podstawowe użycie

v-model może być używany w komponencie do implementacji dwukierunkowego wiązania danych.

Począwszy od Vue 3.4, zalecanym podejściem do osiągnięcia tego jest użycie makra defineModel():

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

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

<template>
  <div>Związany v-model rodzica to: {{ model }}</div>
  <button @click="update">Zwiększ</button>
</template>

Rodzic może następnie powiązać wartość za pomocą v-model:

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

Wartość zwracana przez defineModel() jest referencją (ref). Może być ona odczytywana i modyfikowana jak każda inna referencja, z tą różnicą, że działa jako dwukierunkowe wiązanie między wartością rodzica a wartością lokalną:

  • Jej .value jest zsynchronizowana z wartością powiązaną przez rodzica za pomocą v-model;
  • Kiedy jest modyfikowana przez dziecko, powoduje również aktualizację wartości powiązanej w rodzicu.

Oznacza to, że możesz również powiązać tę referencję z natywnym elementem input za pomocą v-model, co ułatwia opakowywanie natywnych elementów input przy zachowaniu takiego samego sposobu użycia v-model:

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

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

Wypróbuj na playground

Pod maską

defineModel jest makrem dla wygody. Kompilator rozwija je do następujących elementów:

  • Właściwości o nazwie modelValue, z którą synchronizowana jest wartość lokalnej referencji;
  • Zdarzenia o nazwie update:modelValue, które jest emitowane, gdy wartość lokalnej referencji jest modyfikowana.

Tak wyglądałaby implementacja tego samego komponentu potomnego pokazanego powyżej przed wersją 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>

Następnie v-model="foo" w komponencie rodzica zostanie skompilowany do:

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

Jak widać, jest to znacznie bardziej rozwlekłe. Jednak pomocne jest zrozumienie, co dzieje się pod maską.

Ponieważ defineModel deklaruje właściwość (prop), możesz zadeklarować opcje bazowej właściwości, przekazując je do defineModel:

js
// v-model jako wymagane
const model = defineModel({ required: true })

// zdefiniowana wartość podstawowa
const model = defineModel({ default: 0 })

WARNING

Jeśli masz wartość default dla właściwości defineModel i nie przekazujesz żadnej wartości dla tej właściwości z komponentu rodzica, może to spowodować desynchronizację między komponentami rodzica i potomka. W poniższym przykładzie, myRef rodzica jest niezdefiniowane (undefined), ale model potomka ma wartość 1:

js
// komponent podrzędny:
const model = defineModel({ default: 1 })

// komponent nadrzędny:
const myRef = ref()
html
<Child v-model="myRef"></Child>

Najpierw przypomnijmy sobie, jak v-model jest używany na natywnym elemencie:

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

Pod maską, kompilator szablonów rozwija v-model do bardziej rozwlekłego odpowiednika. Więc powyższy kod robi to samo co poniższy:

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

When used on a component, v-model instead expands to this:

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

Aby to faktycznie zadziałało, komponent <CustomInput> musi zrobić dwie rzeczy:

  1. Powiązać atrybut value natywnego elementu <input> z właściwością modelValue
  2. Kiedy zostanie wywołane natywne zdarzenie input, wyemitować własne zdarzenie update:modelValue z nową wartością

Oto jak to działa w praktyce:

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

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

Teraz v-model powinien działać idealnie z tym komponentem:

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

Wypróbuj na Playground

Innym sposobem implementacji v-model w tym komponencie jest użycie zapisywalnej właściwości computed zawierającej zarówno getter jak i setter. Metoda get powinna zwracać właściwość modelValue, a metoda set powinna emitować odpowiednie zdarzenie:

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>

Argumenty v-model

v-model w komponencie może również przyjmować argument:

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

W komponencie potomnym możemy obsłużyć odpowiedni argument przekazując ciąg znaków do defineModel() jako jego pierwszy argument:

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

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

Wypróbuj na Playground

Jeśli potrzebne są również opcje właściwości (prop), powinny być one przekazane po nazwie modelu:

js
const title = defineModel('title', { required: true })
Użycie przed wersją 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>

Wypróbuj na Playground

W tym przypadku, zamiast domyślnej właściwości modelValue i zdarzenia update:modelValue, komponent potomny powinien oczekiwać właściwości title i emitować zdarzenie update:title, aby zaktualizować wartość w komponencie nadrzędnym:

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>

Wypróbuj na Playground

Wiele wiązań v-model

Wykorzystując możliwość wskazania konkretnej właściwości (prop) i zdarzenia, którą poznaliśmy wcześniej w sekcji argumenty v-model, możemy teraz utworzyć wiele wiązań v-model w jednej instancji komponentu.

Każdy v-model będzie synchronizował się z inną właściwością, bez potrzeby dodatkowych opcji w komponencie:

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>

Wypróbuj na Playground

Użycie przed wersją 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>

Wypróbuj na 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>

Wypróbuj na Playground

Obsługa modyfikatorów v-model

Podczas nauki o wiązaniach danych formularza poznaliśmy wbudowane modyfikatory dla v-model - .trim, .number i .lazy. W niektórych przypadkach możesz również chcieć, aby v-model w twoim własnym komponencie wejściowym obsługiwał niestandardowe modyfikatory.

Stwórzmy przykładowy niestandardowy modyfikator capitalize, który zmienia pierwszą literę ciągu znaków dostarczonego przez wiązanie v-model na wielką:

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

Modyfikatory dodane do v-model komponentu są dostępne w komponencie potomnym poprzez destrukturyzację wartości zwracanej przez defineModel() w następujący sposób:

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

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

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

Aby warunkowo dostosować sposób odczytu/zapisu wartości w zależności od modyfikatorów, możemy przekazać opcje get i set do defineModel(). Te dwie opcje otrzymują wartość podczas operacji get/set referencji modelu i powinny zwracać przekształconą wartość. Oto jak możemy wykorzystać opcję set do zaimplementowania modyfikatora 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>

Wypróbuj na Playground

Użycie przed wersją 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="modelValue" @input="emitValue" />
</template>

Wypróbuj na Playground

Modyfikatory dodane do v-model komponentu będą dostarczone do komponentu poprzez właściwość modelModifiers. W poniższym przykładzie stworzyliśmy komponent, który zawiera właściwość modelModifiers z domyślną wartością pustego obiektu:

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>

Zauważ, że właściwość modelModifiers komponentu zawiera capitalize o wartości true - ponieważ zostało to ustawione w wiązaniu v-model jako v-model.capitalize="myText".

Teraz, gdy mamy skonfigurowaną naszą właściwość, możemy sprawdzić klucze obiektu modelModifiers i napisać procedurę obsługi, która zmieni emitowaną wartość. W poniższym kodzie będziemy zmieniać pierwszą literę ciągu na wielką za każdym razem, gdy element <input /> wyemituje zdarzenie 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>

Wypróbuj na Playground

Modyfikatory dla v-model z argumentami

Dla wiązań v-model zawierających zarówno argument jak i modyfikatory, wygenerowana nazwa właściwości będzie miała postać arg + "Modifiers". Na przykład:

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

Odpowiednie deklaracje powinny być:

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

Oto kolejny przykład użycia modyfikatorów z wieloma v-model z różnymi argumentami:

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>
Użycie przed wersją 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 w komponentachJest załadowany