Saltar al contenido

Componente v-model

Uso Básico

v-model puede usarse en un componente para implementar un enlace bidireccional.

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

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

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

<template>
  <div>El v-model del padre es: {{ model }}</div>
  <button @click="update">Incrementar</button>
</template>

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

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

El valor devuelto por defineModel() es una ref. Se puede acceder y mutar como cualquier otra ref, excepto que actúa como un enlace bidireccional entre un valor padre y uno local:

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

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

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

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

Pruébalo en el parque de pruebas

Internamente

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

  • Una prop llamada modelValue, con la que se sincroniza el valor de la ref local;
  • Un evento llamado update:modelValue, que se emite cuando el valor de la ref local es mutado.

Así es como implementarías el mismo componente hijo mostrado anteriormente antes de la versión 3.4:

Child.vue
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>

Entonces, v-model="foo" en el componente padre se compilará a:

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

Como puedes ver, es un poco más verboso. Sin embargo, es útil entender lo que sucede internamente.

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

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

// proporcionando un valor por defecto
const model = defineModel({ default: 0 })

WARNING

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

Child.vue
vue
<script setup>
const model = defineModel({ default: 1 })
</script>
Parent.vue
vue
<script setup>
const myRef = ref()
</script>

<template>
  <Child v-model="myRef"></Child>
</template>

Primero, revisemos cómo se usa v-model en un elemento nativo:

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

Internamente, el compilador de templates expande v-model a su equivalente más verboso. Así que el código anterior hace lo mismo que lo siguiente:

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

Cuando se usa 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, el componente <CustomInput> debe hacer dos cosas:

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

Aquí está en acción:

CustomInput.vue
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 el Playground

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

CustomInput.vue
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

v-model en un componente también puede aceptar un argumento:

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

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

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

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

Pruébalo en el Playground

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

js
const title = defineModel('title', { required: true })
Uso anterior a 3.4
MyComponent.vue
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 el Playground

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:

MyComponent.vue
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 el Playground

Múltiples Enlaces v-model

Aprovechando la capacidad de apuntar a una prop y evento particulares, como aprendimos antes 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 necesidad de opciones adicionales en el componente:

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>

Pruébalo en el Playground

Uso anterior 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 el 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>

Pruébalo en el Playground

Manejo de los Modificadores de v-model

Cuando aprendimos sobre los enlaces de entrada de formulario, vimos que v-model tiene modificadores integrados: .trim, .number y .lazy. En algunos casos, también podrías querer que el v-model en tu componente de entrada personalizado admita modificadores personalizados.

Creemos un ejemplo de modificador personalizado, capitalize, que pone en mayúscula la primera letra de la cadena proporcionada por el enlace v-model:

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

Los modificadores añadidos a un v-model de componente se pueden acceder en el componente hijo desestructurando el 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 se debe leer/escribir el valor basándose en los modificadores, podemos pasar las opciones get y set a defineModel(). Estas dos opciones reciben el valor al obtener/establecer la ref del modelo y deben devolver un valor transformado. Así es como podemos usar 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 el Playground

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

Pruébalo en el Playground

Los modificadores añadidos a un v-model de componente se proporcionarán al componente a través de la prop modelModifiers. En el ejemplo siguiente, hemos creado un componente que contiene una prop 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 prop modelModifiers del componente contiene capitalize y su valor es true, debido a que se establece en el enlace v-model v-model.capitalize="myText".

Ahora que tenemos nuestra prop configurada, podemos verificar las claves del objeto modelModifiers y escribir un handler para cambiar el valor emitido. En el código siguiente, pondremos en mayúscula la cadena cada vez que el elemento <input /> dispare 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 el Playground

Modificadores para v-model con Argumentos

Para los enlaces v-model con argumentos y modificadores, el nombre de la prop 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í hay 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 anterior 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
FREE WEEKEND
Unlimited access to ALL Vue School courses
Reserve Your Spot
03
hours
:
21
minutes
: