Skip to content

Componente v-model

Uso básico

v-model se puede usar en un componente para implementar una vinculación bidireccional.

A partir de Vue 3.4, el método recomendado para lograr esto es usando la macro defineModel():

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

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

<template>
  <div>El `v-model` vinculado al padre es: {{ model }}</div>
</template>

El padre puede entonces enlazar un valor con v-model:

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

El valor devuelto por defineModel() es una referencia (ref). Puede ser accedido y mutado como cualquier otra referencia, excepto que actúa como un enlace bidireccional entre un valor del padre y uno local:

  • Su .value está sincronizado con el valor enlazado por el v-model del padre;
  • Cuando es mutado por el hijo, también hace que el valor enlazado del padre se actualice.

Esto significa que también puedes enlazar esta referencia a un elemento de entrada nativo con v-model, lo que facilita envolver elementos de entrada nativos mientras se proporciona el mismo uso de v-model:

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

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

Pruébalo en la Zona de Práctica

Detrás de escena

defineModel es un macro de conveniencia. El compilador lo expande a lo siguiente:

  • Una propiedad llamada modelValue, con la cual se sincroniza el valor del ref local;
  • Un evento llamado update:modelValue, que se emite cuando se muta el valor del ref local.

Así es como implementarías el mismo componente hijo mostrado anteriormente antes de la versión 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>

Como puedes ver, es un poco más largo. Sin embargo, es útil entender qué está sucediendo internamente.

Dado que defineModel declara una prop, puedes declarar las opciones de la prop asociadas pasándola a defineModel:

js
// Haciendo que el v-model sea obligatorio
const model = defineModel({ required: true })

// Proporcionando un valor predeterminado
const model = defineModel({ default: 0 })

WARNING

Si tienes un valor default para la prop defineModel y no proporcionas ningún valor para esta prop desde el componente padre, puede causar una desincronización entre el componente padre y el componente hijo. En el ejemplo siguiente, el myRef del padre es indefinido, pero el model del hijo es 1:

js
// componente hijo:
const model = defineModel({ default: 1 })

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

Primero, volvamos a revisar cómo se utiliza v-model en un elemento nativo:

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

Internamente, el compilador de plantillas expande v-model a su equivalente más detallado para nosotros. Entonces, el código anterior hace lo mismo que lo siguiente:

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

Cuando se utiliza en un componente, v-model se expande a esto:

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

Sin embargo, para que esto funcione realmente, el componente <CustomInput> debe hacer dos cosas:

  1. Vincular el atributo value de un elemento nativo <input> a la propiedad modelValue
  2. Cuando se desencadena un evento input nativo, emitir un evento personalizado update:modelValue con el nuevo valor

Aquí está en acción:

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

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

Ahora v-model debería funcionar perfectamente con este componente:

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

Pruébalo en la Zona de Práctica

Otra forma de implementar v-model dentro de este componente es usar una propiedad computada modificable con un getter y un setter. El método get debería devolver la propiedad modelValue y el método set debería emitir el evento correspondiente:

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>

Argumentos de v-model

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

En un componente hijo, podemos admitir el argumento correspondiente pasando una cadena a defineModel() como su primer argumento:

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

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

Pruébalo en la Zona de Práctica

Si también se necesitan opciones de prop, deben pasarse después del nombre del model:

js
const title = defineModel('title', { required: true })
Uso previo a 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>

Pruébalo en la Zona de Práctica

En este caso, en lugar de la prop modelValue predeterminada y el evento update:modelValue, el componente hijo debe esperar una prop title y emitir un evento update:title para actualizar el valor del padre:

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>

Pruébalo en la Zona de Práctica

Multiples vinculaciones a v-model

vue
<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>

Al aprovechar la capacidad de apuntar a una prop y evento específicos como aprendimos anteriormente con los argumentos de v-model, ahora podemos crear múltiples enlaces v-model en una sola instancia de componente.

Cada v-model se sincronizará con una prop diferente, sin la necesidad de opciones adicionales en el componente:

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>

Pruébalo en la Zona de Práctica

Uso previo a 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>

Pruébalo en la Zona de Práctica

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>

Pruébalo en la Zona de Práctica

Manejo de modificadores de v-model

Cuando estábamos aprendiendo sobre las vinculaciones de entrada de formularios, vimos que v-model tiene modificadores integrados: .trim, .number y .lazy. En algunos casos, es posible que también desees que el v-model en tu componente de entrada personalizado admita modificadores personalizados.

Creemos un ejemplo de modificador personalizado, capitalize, que capitalice la primera letra de la cadena de texto proporcionada por la vinculación de v-model:

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

Los modificadores agregados a un componente v-model pueden ser accedidos en el componente hijo mediante la destructuración del valor de retorno de defineModel(), de esta manera:

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

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

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

Para ajustar condicionalmente cómo debe ser leído o escrito el valor en función de los modificadores, podemos pasar opciones get y set a defineModel(). Estas dos opciones reciben el valor en la obtención / establecimiento de la ref del model y deben devolver un valor transformado. Así es como podemos utilizar la opción set para implementar el modificador 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>

Pruébalo en la Zona de Práctica

Uso previo a 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>

Pruébalo en la Zona de Práctica

Los modificadores agregados a un componente v-model serán proporcionados al componente a través de la propiedad modelModifiers. En el ejemplo a continuación, hemos creado un componente que contiene una propiedad modelModifiers que por defecto es un objeto vacío:

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>

Observa que la propiedad modelModifiers del componente contiene capitalize y su valor es true, debido a que se estableció en la vinculación v-model como v-model.capitalize="myText".

Ahora que tenemos nuestra propiedad configurada, podemos verificar las claves del objeto modelModifiers y escribir un controlador para cambiar el valor emitido. En el código a continuación, capitalizaremos la cadena de texto cada vez que el elemento <input /> genere un evento 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>

Pruébalo en la Zona de Práctica

Modificadores para v-model con argumentos

Para las vinculaciones v-model con argumento y modificadores, el nombre de la propiedad generada será arg + "Modifiers". Por ejemplo:

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

Las declaraciones correspondientes deberían ser:

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

Aquí tienes otro ejemplo de cómo usar modificadores con múltiples v-model con diferentes argumentos:

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>
Uso previo a 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>
Componente v-model has loaded