Para poder tener sincronizado el formulario con nuestros datos utilizamos la directiva v-model en cada campo. Algunos modificadores útiles de v-model son:
Vamos a ver cómo usar los diferentes tipos de campos con Vue.
Podemos probar el resultado en la documentación de Vue.
En este caso simplemente añadimos la directiva v-model al input:
<label>Nombre:</label>
<input type="text" v-model="user.nombre">
Ponemos en todos los radiobuttons el v-model y a cada uno el value que se guardará al escoger dicha opción:
<label>Sexo:</label>
<input type="radio" value="H" name="sexo" v-model="user.sexo">Hombre
<input type="radio" value="M" name="sexo" v-model="user.sexo">Mujer
Igual que cualquier input, le ponemos el v-model. Si no ponemos un value los valores que se guardarán serán true si está marcado y false si no lo está:
<input type="checkbox" v-model="user.acepto">Acepto las condiciones
Se trata de varios checkbox pero cuyos valores se guardan en el mismo campo, que debe ser un array. Le ponemos el v-model y el value que queramos que se guarde. La variable (en este ejemplo user.ciclos será un array y guardará el value de cada checkbox marcado:
<input type="checkbox" v-model="user.ciclos" value="smx">Sistemas Microinformáticos y Redes<br>
<input type="checkbox" v-model="user.ciclos" value="asix">Administración de Sistemas Informáticos y Redes<br>
<input type="checkbox" v-model="user.ciclos" value="dam">Desarrollo de Aplicaciones Multiplataforma<br>
<input type="checkbox" v-model="user.ciclos" value="daw">Desarrollo de Aplicaciones Web<br>
Si tenemos marcadas las casillas 1 y 3 el valor de user.ciclos será [‘smx’, ‘dam’].
Muchas veces las opciones a mostrar las tendremos en algún objeto (una tabla de la BBDD, …). En ese caso podemos generar automáticamente un checkbox para cada elemento:
ciclos: [
{cod: 'smx', desc: 'Sist. Microinformáticos y Redes'},
{cod: 'asix', desc: 'Adm. de Sistemas Informáticos y Redes'},
{cod: 'dam', desc: 'Desar. de Aplicaciones Multiplataforma'},
{cod: 'daw', desc: 'Desar. de Aplicaciones Web'},
]
<div v-for="ciclo in ciclos" :key="ciclo.cod">
<input type="checkbox" v-model="user.ciclos" :value="ciclo.cod">{ { ciclo.desc }}<br>
</div>
Lo único que hay que hacer es poner al select la directiva v-model:
<select v-model="user.tutor">
<option value=''>No es tutor</option>
<option value="smx">Sistemas Microinformáticos y Redes</option>
<option value="asix">Administración de Sistemas Informáticos y Redes</option>
<option value="dam">Desarrollo de Aplicaciones Multiplataforma</option>
<option value="daw">Desarrollo de Aplicaciones Web</option>
</select>
También podemos generar las opciones automáticamente:
<select v-model="user.tutor">
<option value=''>No es tutor</option>
<option v-for="ciclo in ciclos" :key="ciclo.cod" :value="ciclo.cod">
{ { ciclo.desc }}
</option>
</select>
Si queremos que sea un select múltiple sólo tenemos que ponerle el atributo multiple a la etiqueta <select> y hacer que la variable user.tutor sea un array, que se comportará como en los checkbox múltiples.
Podemos validar el formulario “a mano” como hemos visto en JS:
Además deberíamos poner clase de error a los inputs con errores para destacarlos, poner el cursor en el primer input erróneo, etc.
Todo esto es incómodo y poco productivo. Para mejorarlo podemos usar una de las muchísimas librerías para validación de formularios como:
Tenéis toda la información así como un tutorial de cómo usar este librería en la documentación de VeeValidate).
La forma de instalarla es
npm install vee-validate -S
Y para usarla simplemente cambiaremos la etiqueta <input>
por el componente <Field>
y la etiqueta <form>
por el componente <Form>
pero quitándole el modificador .prevent
del escuchador @submit
y haciendo que la función manejadora reciba un parámetro llamado values que es un objeto con los valores de los inputs del formulario.
Cada componente Field necesitará un atributo name
que es el nombre del campo con el valor de ese input dentro del objeto values. Si el formulario es sólo para recoger datos, no para modificar datos existentes no necesitamos la directiva v-model
porque sus valores se guardarán en el objeto values que recibe la función manejadora del @submit
. Sin embargo si debe mostrar datos que pueden cambiar tras la carga del componente mantendremos el atributo v-model
(como en la práctica que estamos haciendo, que si nos pasan una id cargamos el libro con dicha id y lo mostramos en el formlario para editarlo).
Para validar un campo se le añade al componente un atributo :rules
con la función a ejecutar, que devolverá el mensaje a mostrar en caso de error o true si es correcto. El mensaje se mostrará en un componente llamado ErrorMessage
(que deberemos importar y registrar) cuyo atributo name
debe ser igual al del campo a validar. Si alguna de las funciones de validación no devuelve true no se ejecuta la función manejadora del submit.
Habrá que importar los componentes de'vee-validate'
que se usen (Form, Field, ErrorMessage) y registrarlos.
Si no usamos v-model
podemos darle un valor por defecto a los inputs (por ejemplo, si estamos editando un objeto que ya tiene valores) pasándole el objeto con los valores al componente <Form>
en un atributo llamado initial-values
. Pero si cambien esos valores tras cargar el componente no se reflejarán los cambios (para ello debemos usar v-model
).
Por ejemplo si estamos editando el objeto
product = {
name: 'Ratón óptico',
price: '8.95'
}
el formulario sería:
<Form :initial-values="product" @submit="onSubmit">
<Field name="name" type="text" />
<ErrorMessage name="name" />
<Field name="price" type="text" />
<ErrorMessage name="price" />
<button type="submit">Guardar</button>
</Form>
Si el objeto product está vacío el formulario aparecerá en blanco pero si contiene datos se mostrarán en el formulario. Sin embargo si modificamos los datos de product esos cambios no se reflejan en el formlario a menos que usemos v-model
.
A continuación tenéis un ejemplo completo de un formulario para validar un email y una contraseña (Fuente https://codesandbox.io/s/vee-validate-basic-example-nc7eh?from-embed=&file=/src/App.vue):
<template>
<div id="app">
<Form @submit="onSubmit">
<Field name="email" type="email" :rules="validateEmail" />
<ErrorMessage name="email" />
<Field name="password" type="password" :rules="validatePassword" />
<ErrorMessage name="password" />
<button>Sign up</button>
</Form>
</div>
</template>
<script>
import { Form, Field, ErrorMessage } from "vee-validate";
export default {
components: {
Form,
Field,
ErrorMessage,
},
methods: {
onSubmit(values) {
console.log(values);
},
validateEmail(value) {
// if the field is empty
if (!value) {
return "This field is required";
}
// if the field is not a valid email
const regex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i;
if (!regex.test(value)) {
return "This field must be a valid email";
}
// All is good
return true;
},
validatePassword(value) {
// if the field is empty
if (!value) {
return "This field is required";
}
// if the length is less than 8 characters
if (value.length < 8) {
return "The length of this field must be at least 8 characters";
}
// All is good
return true;
}
},
};
</script>
Podemos encontrar más información sobre vee-validate en su documentación oficial.
Para validar un <select>
simplemente lo cambiamos por un <Field as="select">
. Ejemplo:
<Field as="select" name="autor" class="form-control" required>
<option value="">--- Selecciona autor ---</option>
<option v-for="autor in autores" :key="autor.id"
:value="autor.id">
</option>
</Field>
<ErrorMessage name="autor" />
Para un textarea pondremos un <Field as="textarea">
.
En el caso de un checkbox o un radiobutton simplemente añadimos al Field
un atributo type
indicando su tipo:
<Field name="drink" type="radio" value="Water" /> Water
<Field name="drink" type="radio" value="Tea" /> Tea
<Field name="drink" type="radio" value="Coffee" /> Coffee
Si se trata de varios checkbox con el mismo atributo name en values se recibirá un array con los values de los elementos marcados.
El problema de validar los datos así es que tenemos varias funciones independientes que validan los distintos inputs lo que dispersa el código de la vaidación.
Podemos ponerlas todas como propiedades de un objeto que le pasamos como atributo al Form
, evitando además tener que poner los atributos rules
en cada Field
a validar.
El ejemplo anterior quedaría:
<template>
<div id="app">
<Form :validation-schema="mySchema" @submit="onSubmit">
<Field name="email" type="email" />
<ErrorMessage name="email" />
<Field name="password" type="password" />
<ErrorMessage name="password" />
<button>Sign up</button>
</Form>
</div>
</template>
<script>
import { Form, Field, ErrorMessage } from "vee-validate";
export default {
components: {
Form,
Field,
ErrorMessage,
},
data() {
return {
mySchema = {
email: (value) => {
if (!value) return "This field is required";
const regex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i;
if (!regex.test(value)) return "This field must be a valid email";
return true;
},
password: (value) => {
if (!value) return "This field is required";
if (value.length < 8) return "The length of this field must be at least 8 characters";
return true;
}
}
}
},
methods: {
onSubmit(values) {
console.log(values);
},
},
};
</script>
Vee-validate 4 también permite usar librerías como yup. En este caso la validación es casi automática como se muestra en la documentación de vee-validate. El ejemplo anterior quedaría:
<template>
<div id="app">
<Form @submit="onSubmit" :validation-schema="mySchema">
<Field name="email" type="email" />
<ErrorMessage name="email" />
<Field name="password" type="password" />
<ErrorMessage name="password" />
<button>Sign up</button>
</Form>
</div>
</template>
<script>
import { Form, Field, ErrorMessage } from "vee-validate";
import * as yup from 'yup';
export default {
components: {
Form,
Field,
ErrorMessage,
},
data() {
const mySchema = yup.object({
email: yup.string().required().email(),
password: yup.string().required().min(8),
})
return {
mySchema
}
},
methods: {
onSubmit(values) {
console.log(values);
},
},
};
</script>
Para personalizar un mensaje de error de un campo sólo tenemos que indicar el mensaje al definir la regla del campo:
const mySchema = yup.object({
email: yup.string().required('El email es obligatorio').email(),
password: yup.string().required().min(8, 'La contraseña debe tener al menos 8 caracteres'),
})
En este caso hemos personalizado el mensaje del email si no contiene nada y del password si no cumple el min.
Si queremos personalizar todos los mensajes de error debemos definir un objeto con los nuevos mensajes. Las validaciones no incluidas mantendrán el mensaje original. Ejemplo:
import * as yup from 'yup';
import { setLocale } from 'yup';
setLocale({
mixed: {
default: 'Campo no válido',
required: 'El campo ${path} no puede estar vacío'
},
string: { // sólo las reglas 'min' de campos 'string'
min: 'El campo ${path} debe tener al menos ${max} caracteres'
},
number: { // sólo las reglas 'min' de campos 'number'
min: 'El valor del campo debe ser mayor que ${min}',
},
});
Si lo que queremos validar no lo hace ningún validador de yup podemos crear nuestra propia regla usando el validador test()
que como 1º parámetro recibe el nombre de la regla, como 2º el mensaje de error a mostrar y como 3º una función que recibe el valor del campo y devolverá true/false indicando si es válido o no. Por ejemplo el campo seed debe ser múltiplo de 7:
const mySchema = yup.object({
seed: yup.number().required().test('seven-multiplo', 'El valor debe ser múltiplo de 7', (value) => {
return !(value % 7)
},
...
})
La forma enlazar cada input con su variable correspondiente es mediante la directiva v-model que hace un enlace bidireccional: al cambiar la variable Vue cambia el valor del input y si el usuario cambia el input Vue actualiza la variable automáticamente.
El problema lo tenemos si hacemos que los inputs estén en subcomponentes. Si ponemos allí el v-model al escribir en el input se modifica el valor de la variable en el subcomponente (que es donde está el v-model) pero no en el padre.
Para solucionar este problema tenemos 2 opciones: imitar nosotros en el subcomponente lo que hace v-model o utilizar slots.
Como los cambios en el subcomponente no se transmiten al componente padre hay que emitir un evento desde el subcomponente que escuche el padre y que proceda a hacer el cambio en la variable.
La solución es imitar lo que hace un v-model que en realidad está formado por:
Así que lo que haremos es:
<form-input v-model="campo"></form-input>
<template>
<div class="control-group">
<!-- id -->
<label class="control-label" :for="nombre">{ { titulo }}</label>
<div class="controls">
<input :value="value" @input="$emit('input', $event.target.value)" type="text" :id="nombre" :name="nombre" placeholder="Escribe el nombre" class="form-control">
</div>
</div>
</template>
props: ['value'],
Componente padre: formulario
<form class="form-horizontal">
<form-input v-model="user.id" titulo="Id" nombre="id"></form-input>
<form-input v-model="user.name" titulo="Nombre" nombre="name"></form-input>
<form-input v-model="user.username" titulo="Username" nombre="username"></form-input>
<form-input v-model="user.email" titulo="E-mail" nombre="email"></form-input>
<form-input v-model="user.phone" titulo="Teléfono" nombre="phone"></form-input>
<form-input v-model="user.website" titulo="URL" nombre="website"></form-input>
<form-input v-model="user.companyName" titulo="Nombre de la empresa" nombre="nomEmpresa"></form-input>
</form>
Subcomponente: form-input
<template>
<div class="control-group">
<label class="control-label" :for="nombre">{ { titulo }}</label>
<div class="controls">
<input :value="value" @input="updateValue($event.target.value)" type="text" :id="nombre" :name="nombre" placeholder="" class="form-control">
</div>
</div>
</template>
<script>
export default {
name: 'user-form-input',
props: ['value', 'titulo', 'nombre'],
methods: {
updateValue(value) {
this.$emit('input', value)
}
}
}
</script>
Esto mismo podemos hacer si estamos usando VeeValidate para validar nuestro formulario. Tenemos toda la información en la documentación oficial.
Ya vimos al hablar de la comunicación entre componentes que un slot es una ranura en un subcomponente que, al renderizarse, se rellena con lo que le pasa el padre.
Esto podemos usarlo en los formularios de forma que el <input> con el v-model lo pongamos en un slot de forma que está enlazado correctamente en el padre.
Componente padre: formulario
<form class="form-horizontal">
<form-input titulo="Id">
<input v-model="user.id" type="text" id="id" name="id" class="form-control" disabled>
</form-input>
<form-input titulo="Nombre">
<input v-model="user.name" type="text" id="name" name="name" class="form-control">
</form-input>
<form-input titulo="Username">
<input v-model="user.username" type="text" id="username" name="username" class="form-control">
</form-input>
<form-input titulo="E-mail">
<input v-model="user.email" type="email" id="email" name="email" class="form-control">
</form-input>
<form-input titulo="Teléfono">
<input v-model="user.phone" type="text" id="phone" name="phone" class="form-control">
</form-input>
<form-input titulo="URL">
<input v-model="user.website" type="text" id="website" name="website" class="form-control">
</form-input>
<form-input titulo="Nombre de la empresa">
<input v-model="user.companyName" type="text" id="nomEmpresa" name="nomEmpresa" class="form-control">
</form-input>
</form>
Subcomponente: form-input
<template>
<div class="control-group">
<label class="control-label">{ { titulo }}</label>
<div class="controls">
<slot>Aquí va un INPUT</slot>
</div>
</div>
</template>
<script>
export default {
name: 'user-form-input',
props: ['value', 'titulo', 'nombre'],
methods: {
updateValue(value) {
this.$emit('input', value)
}
}
}
</script>