Passer au contenu

v-model du composant

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

Tout d'abord, revoyons comment 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
  :modelValue="searchText"
  @update:modelValue="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>
vue
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['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" />

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

Les arguments de v-model

Par défaut, v-model sur un composant utilise modelValue comme prop et update:modelValue comme évènement. Nous pouvons modifier ces noms en passant un argument à v-model :

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

Dans ce cas, le composant enfant doit attendre une prop title et émettre un évènement update:title pour mettre à jour la valeur du composant parent :

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

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

Essayer en ligne

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>
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 v-model de composant seront fournis au composant via la prop modelModifiers. Dans l'exemple ci-dessous, nous avons créé un composant qui contient une prop modelModifiers qui par défaut est un objet vide :

vue
<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>
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 - car elle est définie sur la liaison v-model v-model.capitalize="myText".

Maintenant que notre prop est configurée, nous pouvons vérifier les clés de l'objet modelModifiers et écrire un gestionnaire pour modifier la valeur émise. Dans le code ci-dessous, nous mettrons la chaîne de caractères en majuscule chaque fois que l'élément <input /> déclenche un évènement input.

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

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 liaisons v-model avec à la fois des arguments et des modificateurs, 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
const props = defineProps(['title', 'titleModifiers'])
defineEmits(['update:title'])

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

Here's another example of using modifiers with multiple v-model with different arguments:

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