Passer au contenu

v-model du composant

Utilisation de base

v-model peut être utilisé sur un composant pour implémenter une liaison à double sens.

À partir de Vue 3.4, l'approche recommandée consiste à utiliser la macro defineModel() :

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

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

<template>
  <div>Le v-model lié au parent est : {{ model }}</div>
</template>

Le parent peut alors lier une valeur avec v-model :

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

La valeur retournée par defineModel() est une ref. On peut y accéder et la modifier comme n'importe quel autre ref, sauf qu'elle agit comme une liaison bidirectionnelle entre une valeur parent et une valeur locale :

  • Sa .value est synchronisée avec la valeur liée au v-model parent ;
  • Lorsqu'elle est modifiée par l'enfant, la valeur liée au parent est également mise à jour.

Cela signifie que vous pouvez également lier cette ref à un élément d'entrée natif avec v-model, ce qui permet d'envelopper les éléments d'entrée natifs tout en fournissant la même utilisation de v-model :

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

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

Essayer en ligne

Sous le capot

defineModel est une macro de commodité. Le compilateur l'étend de la manière suivante :

  • Une propriété nommée modelValue, avec laquelle la valeur de la ref locale est synchronisée ;
  • Un événement nommé update:modelValue, qui est émis lorsque la valeur de la ref locale est modifiée.

C'est ainsi que l'on mettrait en œuvre le même composant enfant que celui présenté ci-dessus avant la version 3.4 :

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>

Comme vous pouvez le voir, c'est un peu plus verbeux. Cependant, il est utile de comprendre ce qui se passe sous le capot.

Puisque defineModel déclare une prop, vous pouvez donc déclarer les options de la prop sous-jacente en la passant à defineModel :

js
// rendre v-model obligatoire
const model = defineModel({ required: true })

// fournir une valeur par défaut
const model = defineModel({ default: 0 })

WARNING

Si vous avez une valeur default pour la propriété defineModel et que vous ne fournissez aucune valeur pour cette propriété à partir du composant parent, cela peut provoquer une désynchronisation entre les composants parent et enfant. Dans l'exemple ci-dessous, le composant parent myRef est undefined, mais le composant enfant model vaut 1 :

js
// composant enfant :
const model = defineModel({ default: 1 })

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

Revenons tout d'abord sur la façon dont v-model est utilisé sur un élément natif :

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

Sous le capot, le compilateur de template transforme v-model en un équivalent plus verbeux pour nous. Ainsi, le code ci-dessus fait la même chose que ce qui suit :

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

Lorsqu'il est utilisé sur un composant, v-model est alors équivalent à :

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

Toutefois, pour que cela fonctionne, le composant <CustomInput> doit faire deux choses :

  1. Lier l'attribut value d'un élément natif <input> à la prop modelValue
  2. Lorsqu'un événement natif input est déclenché, émettre un événement personnalisé update:modelValue avec la nouvelle valeur

Voici cela en action :

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

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

Maintenant, v-model devrait fonctionner parfaitement avec ce composant :

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

Essayer en ligne

Une autre façon d'implémenter v-model dans ce composant consiste à utiliser une propriété calculée en écriture avec à la fois un accesseur et un mutateur. La méthode get doit renvoyer la propriété modelValue et la méthode set doit émettre l'événement correspondant :

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>

Les arguments de v-model

v-model sur un composant peut également accepter un argument :

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

Dans le composant enfant, nous pouvons prendre en charge l'argument correspondant en passant une chaîne à defineModel() comme premier argument :

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

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

Essayer en ligne

Si des options de prop sont également nécessaires, elles doivent être transmises après le nom du modèle :

js
const title = defineModel('title', { required: true })
Utilisation avant la 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>

Essayer en ligne

Dans ce cas, au lieu de la prop modelValue et de l'événement update:modelValue par défaut, le composant enfant doit s'attendre à une prop title et émettre un événement update:title pour mettre à jour la valeur du parent :

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>

Essayer en ligne

Liaisons multiple avec v-model

En tirant parti de la possibilité de cibler une prop et un événement en particulier, comme nous l'avons appris précédemment avec les arguments de v-model, nous pouvons désormais créer plusieurs liaisons v-model sur une seule instance de composant.

Chaque v-model se synchronisera avec une prop différente, sans avoir besoin d'options supplémentaires dans le composant :

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>

Essayer en ligne

Utilisation avant la 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>

Essayer en ligne

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>

Essayer en ligne

Gestion des modificateurs de v-model

Lorsque nous avons appris les liaisons d'entrée de formulaire, nous avons vu que v-model avait des modificateurs natifs - .trim, .number et .lazy. Dans certains cas, vous pouvez également souhaiter que le v-model de votre composant d'entrée personnalisé prenne en charge les modificateurs personnalisés.

Créons un exemple de modificateur personnalisé, capitalize, qui met en majuscule la première lettre de la chaîne de caractères fournie par la liaison v-model :

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

Les modificateurs ajoutés à un composant v-model sont accessibles dans le composant enfant en déstructurant la valeur de retour de defineModel() comme suit :

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

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

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

Pour ajuster conditionnellement la façon dont la valeur doit être lue / écrite en fonction des modificateurs, nous pouvons passer les options get et set à defineModel(). Ces deux options reçoivent la valeur sur get / set du modèle ref et doivent retourner une valeur transformée. C'est ainsi que nous pouvons utiliser l'option set pour implémenter le modificateur 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>

Essayer en ligne

Utilisation avant la 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>

Essayer en ligne

Les modificateurs ajoutés à un composant v-model seront fournis au composant via la prop modelModifiers. Dans l'exemple ci-dessous, nous avons créé un composant qui contient une prop modelModifiers qui est par défaut un objet vide :

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>

Notez que la prop modelModifiers du composant contient capitalize et que sa valeur est true - parce qu'elle est définie sur le lien v-model v-model.capitalize="myText".

Maintenant que nous avons mis en place notre prop, nous pouvons vérifier les clés de l'objet modelModifiers et écrire un gestionnaire pour changer la valeur émise. Dans le code ci-dessous, nous mettrons la chaîne en majuscules chaque fois que l'élément <input /> déclenchera un événement 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>

Essayer en ligne

Modificateurs pour v-model avec arguments

Pour les liens v-model avec à la fois un argument et un modificateur, le nom de la prop générée sera arg + "Modifiers". Par exemple :

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

Les déclarations correspondantes doivent être :

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

Voici un autre exemple d'utilisation de modificateurs avec plusieurs v-model ayant des arguments différents :

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>
Utilisation avant la 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 du composanta chargé