Skip to content

Componente v-model

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

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

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

En el fondo, el compilador de plantillas expande v-model al equivalente más descriptivo para nosotros. Así que 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 convierte en esto:

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

Para que esto funcione realmente, el componente <CustomInput> debe hacer dos cosas:

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

Aquí lo tienes 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>
vue
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['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" />

Otra forma de implementar v-model dentro de este componente es utilizar una propiedad computed editable con un getter y un setter. El método get debe retornar la propiedad modelValue y el método set debe 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>
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>

Argumentos de v-model

Por defecto, v-model en un componente utiliza modelValue como prop y update:modelValue como evento. Podemos modificar estos nombres pasando un argumento a v-model:

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

En este caso, el componente hijo debería esperar una props title y emitir un evento update:title para actualizar el valor del padre:

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>

Pruébalo en la Zona de Práctica

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

Múltiples Vinculaciones v-model

Aprovechando la capacidad de apuntar a una prop y a un evento en particular como aprendimos antes en argumentos de v-model, ahora podemos crear múltiples vinculaciones v-model en una sola instancia del 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>
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 los Modificadores de v-model

Cuando aprendimos sobre los enlaces de entrada de formularios, vimos que v-model tiene modificadores integrados: .trim, .number y .lazy. En algunos casos, es posible que también quieras que el v-model de tu componente de entrada personalizado soporte modificadores personalizados.

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

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

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

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>

Observa que la prop del componente modelModifiers contiene capitalize y su valor es true debido a que ha sido configurado en el v-model vinculando v-model.capitalize="myText".

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

Pruébalo en la Zona de Práctica

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 prop generada será arg + "Modifiers". Por ejemplo:

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

Las declaraciones correspondientes deberían ser:

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 }
  }
}

Aquí hay otro ejemplo de uso de 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 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