A día de hoy en muchas webs es esencial el uso de formularios y eso conlleva la creación de validadores para los datos que introducen los usuarios.

Imagina que tienes un campo de texto en la web para que el usuario meta su nombre, y a la vez quieres que como mucho tenga 12 caracteres. Lo suyo sería validar que el texto que mete el usuario no pasa de esa longitud antes de mandarse al servidor, o que al menos directamente no deje al usuario escribir más si llega al límite de caracteres.

Ese ejemplo de validación no es muy complicado, pero si tienes un formulario más complejo, crear todo el sistema de validaciones puede ser complejo, especialmente si no usamos un framework como Vue.

En el artículo de hoy vamos a ver una forma de crear formularios simples con validaciones básicas de los datos. Si eso se te queda corto y necesitas cosas más complejas, también vamos a ver validaciones de los datos usando VeeValidate y VueValidate, dos de las librerías más populares para Vue a la hora de usar formularios.

v-model

Empecemos por lo básico, lo que todo el mundo debería conocer a estas alturas. Imaginemos que tenemos un formulario que por el momento solo tiene un input para escribir. Si queremos recoger el valor que escribe el usuario en tiempo real tenemos que hacer uso de la propiedad v-model.

<template>
  <div>
    <input type="text" v-model="result" placeholder="Escribe algo..." />
    {{ text }}
  </div>
</template>

<script>
export default {
  data: () => ({
    result: ""
  ))
}
</script>

En este ejemplo de arriba lo que hago es usar el v-model con la variable que tengo declarada en el data, en este caso result pero la puedes llamar como quieras. Debajo del input pinto el resultado de esa variable. Si abres la página con este componente y empiezas a escribir, te darás cuenta de que al lado del input se escribe lo mismo que estás escribiendo tu en tiempo real.

Si ahora queremos crear el típico formulario de contacto, tan solo tenemos que crear unos cuantos input cada uno con v-model apuntando a una variable distinta y un botón de input del formulario con el evento de @submit para enviar el resultado o imprimirlo por pantalla. Algo así:

<template>
  <div class="contact">
    <h1 class="title">Contacto</h1>
    <form action class="form" @submit.prevent="contact">
      <label class="form-label" for="#name">Nombre</label>
      <input
        v-model="name"
        class="form-input"
        type="text"
        id="name"
        required
        placeholder="Nombre"
      />
      <label class="form-label" for="#email">Email</label>
      <input
        v-model="email"
        class="form-input"
        type="email"
        id="email"
        placeholder="Email"
      />
      <input class="form-submit" type="submit" value="Contactar" />
    </form>
  </div>
</template>

<script>
export default {
  data: () => ({
    name: "",
    email: "",
  }),
  methods: {
    contact() {
      console.log(this.name);
      console.log(this.email);
    },
  },
};
</script>

Con el prevent del submit se evita que se haga la acción por defecto de los formularios que es la de recargar toda la página.

v-model a través de componentes

Una cosa que no mucha gente conoce de Vue es que puedes hacer v-model desde un componente a otro. Pongamos que creamos un componente en Vue y dentro del template colocamos unicamente un input.

Si no conocías esto que te voy a contar, en esta situación lo que muchos hacen es emitir un evento cada vez que el usuario escribe para poder tener su valor en el componente padre.

Lo malo de esa estrategia es que cada vez que quieres hacer esto tienes que crear un método en el padre para poder recibir el campo que viene en el evento.

Úna solución más elegante a esto consite en crear un v-model de forma global al componente. Realmente cuando usas un v-model lo que pasa internamente es que Vue va a cambiar eso por:

<input v-bind:value="something" v-on:input="something = $event.target.value" />

Lo que nos lleva a que para hacer esto necesitas dos cosas en el componente hijo, el que tiene el input en este ejemplo:

  • Necesitas un prop llamado value que será el valor incicial que tiene el elemento
  • Necesitas que se emita un evento llamado input con el valor modificado para pasar al componente padre

El componente quedaría así:

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

<script>
export default {
  props: ["value"],
};
</script>
// AnotherComponent.vue
<template>
  <div>
    <my-input v-model="name" />
    {{ name }}
  </div>
</template>

<script>
import MyInput from "./MyInput.vue";

export default {
  components: { MyInput },
  data: () => ({
    name: "",
  }),
};
</script>

Como ves arriba, haciando eso puedes crear un v-model para todo un componente creado por ti. Además, tienes la ventaja de que no necesitas poner @input ya que el valor actualizado automáticamente lo tienes dentro de la variable que pasas en el v-model.

Validación de los datos

Una parte fundamental de los formularios es poder validar los datos que introduce el usuario. Esto evita posibles problemas en el servidor y permite informar al usuario de un error en los datos antes de enviarlos.

Para validar los datos básicamente tienes dos formas: de forma manual y con librerías de terceros. Ninguna de estas estrategias es mejor en todas las situcaciones, tienes que leer y entender cómo funcionan para poder elegir la que mejor se adapte a tus necesidades.

De forma manual

Es la forma más compleja pero tiene la ventaja de que lo controlas todo y puedes complicarlo hasta donde quieras.

Ponte que quieres comprobar que la contraseña que ha metido el usuario tiene al menos un número. Lo más sencillo es crear una propiedad computada para que se ejecute cuando cambian las variables del data asociadas. Por ejemplo:

<template>
  <form>
    <label for="#password">Password</label>
    <input type="password" id="password" v-model="editedPassword" />
    <p class="error" v-if="editedPassword && !passwordHasNumbers">
      Error. La contraseña debe tener al menos un número
    </p>
  </form>
</template>

<script>
export default {
  data: () => ({
    editedPassword: null,
  }),
  computed: {
    passwordHasNumbers() {
      return /\d/.test(this.editedPassword);
    },
  },
};
</script>

Sería así sencillo de validar, pero, ¿qué pasa si queremos hacer más validaciones?

Que al final acabamos con componentes muy complejos, con mucho HTML, muchas computadas para cada una de las validaciones, etc.

Normalmente este sistema se suele usar para formularios que no requieran de muchas validaciones, al final tienes que mirar tus necesidades y analizar si te merece la pena usar este sistema.

Usando Veevalidate

https://logaretm.github.io/vee-validate/

Esta librería la he descubierto recientemente y la verdad es que me ha gustado mucho. Sirve para validar inputs y para ayudarte con la gestión de eventos y errores.

Lo primero, descargar la librería en el proyecto:

npm install vee-validate --save

Siguiente paso, registrar esta librería para poder usarla de forma global en el proyecto. Para ello añade en el main.js:

// src/main.js

import Vue from "vue";
import VeeValidate from "vee-validate";
import App from "App.vue";

Vue.use(VeeValidate);

new Vue({
  el: "#app",
  render: (h) => h(App),
});

Empecemos con un ejemplo sencillito. Pongamos que queremos crear un formulario de registro con nombre, apellido, correo y contraseña.

En este ejemplo lo que vamos a validar es que al pulsar el botón ningún campo esté vacío y que ademas el email tenga formato de email. Además vamos a hacer que la contraseña tenga más de 6 caractéres.

Primero te voy a poner el código completo de este componente y ahora paso a explicarlo:

<template>
  <div class="jumbotron">
    <div class="container">
      <div class="row">
        <div class="col-sm-8 offset-sm-2">
          <div>
            <h2>Vue.js + VeeValidate - Form Validation</h2>
            <form @submit.prevent="handleSubmit">
              <div class="form-group">
                <label for="firstName">First Name</label>
                <input
                  type="text"
                  v-model="user.firstName"
                  v-validate="'required'"
                  id="firstName"
                  name="firstName"
                  class="form-control"
                  :class="{
                    'is-invalid': submitted && errors.has('firstName'),
                  }"
                />
                <div
                  v-if="submitted && errors.has('firstName')"
                  class="invalid-feedback"
                >
                  {{ errors.first("firstName") }}
                </div>
              </div>
              <div class="form-group">
                <label for="lastName">Last Name</label>
                <input
                  type="text"
                  v-model="user.lastName"
                  v-validate="'required'"
                  id="lastName"
                  name="lastName"
                  class="form-control"
                  :class="{ 'is-invalid': submitted && errors.has('lastName') }"
                />
                <div
                  v-if="submitted && errors.has('lastName')"
                  class="invalid-feedback"
                >
                  {{ errors.first("lastName") }}
                </div>
              </div>
              <div class="form-group">
                <label for="email">Email</label>
                <input
                  type="email"
                  v-model="user.email"
                  v-validate="'required|email'"
                  id="email"
                  name="email"
                  class="form-control"
                  :class="{ 'is-invalid': submitted && errors.has('email') }"
                />
                <div
                  v-if="submitted && errors.has('email')"
                  class="invalid-feedback"
                >
                  {{ errors.first("email") }}
                </div>
              </div>
              <div class="form-group">
                <label for="password">Password</label>
                <input
                  type="password"
                  v-model="user.password"
                  v-validate="{ required: true, min: 6 }"
                  id="password"
                  name="password"
                  class="form-control"
                  :class="{ 'is-invalid': submitted && errors.has('password') }"
                />
                <div
                  v-if="submitted && errors.has('password')"
                  class="invalid-feedback"
                >
                  {{ errors.first("password") }}
                </div>
              </div>
              <div class="form-group">
                <button class="btn btn-primary">Register</button>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "app",
  data() {
    return {
      user: {
        firstName: "",
        lastName: "",
        email: "",
        password: "",
      },
      submitted: false,
    };
  },
  methods: {
    handleSubmit(e) {
      this.submitted = true;
      this.$validator.validate().then((valid) => {
        if (valid) {
          alert("SUCCESS!! :-)\n\n" + JSON.stringify(this.user));
        }
      });
    },
  },
};
</script>

Lo primero que tiene la vista es un formulario html <form>. Dentro del formulario se usa el sumbit.prevent para poder ejecutar un método del componente al pulsar el botón de registro.

Dentro de formulario están los labels y los input. Dentro de los input, hay una propiedad expecial llamada v-validate que sirve para poder meter validaciones para ese input. Por ejemplo el primer input, para escribir el nombre, tiene la validación de required para poder sacar el error si el usuario no escribe.

El input para escribir el nombre del usuario tiene dos validaciones separadas por |, el required y el validador del email. El input de la contraseña tiene dos validadores también, pero se pasan mediante un objeto para poder configurarlo, en este ejemplo para validar que como mínimo tiene que tener 6 caracteres.

Por último en la vista están los errores, por un lado se añade una clase a los inputs en caso de tener error y por otro debajo del input se muestra el mensaje de error. Para ello usa el objeto global de errores y se coge la clave de error que corresponda. Vee-validate coge para las claves de error el id que le pongas a cada input.

En la vista simplemente se mira al enviar el formulario que no haya ningún error y en caso de que no haya errrores se muestra un alert.

DEMO: https://codesandbox.io/s/nnqnonwn9p

Usando Vuelidate

https://vuelidate.js.org/

Vuelidate tiene un enfoque distinto a vee-validate aunque en esencia se pueden usar para lo mismo. Vee-validate se centra en validar inputs HTML mientras que Vuelidate sirve para validar cualquier valor. Es decir, vee-validate asume que vas a tener inputs asociados a valores mediante v-model, eso permite a vee-validate poder ofrecer más abstracciones, ayudas, estilos y accesibilidad a los inputs.

Vuelidate no tiene todas esas abstracciones pero tiene la ventaja de que también se puede usar para validar valores en el data o en las computadas de los componentes.

Para usar vue-validate lo primero es instalarlo:

npm install vuelidate --save

Luego en el archivo main.js tienes que añadir:

// src/main.js

import Vue from "vue";
import Vuelidate from "vuelidate";

Vue.use(Vuelidate);

Pongamos el mismo ejemplo que antes del formulario y ahora lo comentamos:

<template>
  <div class="jumbotron">
    <div class="container">
      <div class="row">
        <div class="col-sm-8 offset-sm-2">
          <div>
            <h2>Vue.js + Vuelidate - Form Validation</h2>
            <form @submit.prevent="handleSubmit">
              <div class="form-group">
                <label for="firstName">First Name</label>
                <input
                  type="text"
                  v-model="user.firstName"
                  id="firstName"
                  name="firstName"
                  class="form-control"
                  :class="{
                    'is-invalid': submitted && $v.user.firstName.$error,
                  }"
                />
                <div
                  v-if="submitted && !$v.user.firstName.required"
                  class="invalid-feedback"
                >
                  First Name is required
                </div>
              </div>
              <div class="form-group">
                <label for="lastName">Last Name</label>
                <input
                  type="text"
                  v-model="user.lastName"
                  id="lastName"
                  name="lastName"
                  class="form-control"
                  :class="{
                    'is-invalid': submitted && $v.user.lastName.$error,
                  }"
                />
                <div
                  v-if="submitted && !$v.user.lastName.required"
                  class="invalid-feedback"
                >
                  Last Name is required
                </div>
              </div>
              <div class="form-group">
                <label for="email">Email</label>
                <input
                  type="email"
                  v-model="user.email"
                  id="email"
                  name="email"
                  class="form-control"
                  :class="{ 'is-invalid': submitted && $v.user.email.$error }"
                />
                <div
                  v-if="submitted && $v.user.email.$error"
                  class="invalid-feedback"
                >
                  <span v-if="!$v.user.email.required">Email is required</span>
                  <span v-if="!$v.user.email.email">Email is invalid</span>
                </div>
              </div>
              <div class="form-group">
                <label for="password">Password</label>
                <input
                  type="password"
                  v-model="user.password"
                  id="password"
                  name="password"
                  class="form-control"
                  :class="{
                    'is-invalid': submitted && $v.user.password.$error,
                  }"
                />
                <div
                  v-if="submitted && $v.user.password.$error"
                  class="invalid-feedback"
                >
                  <span v-if="!$v.user.password.required"
                    >Password is required</span
                  >
                  <span v-if="!$v.user.password.minLength"
                    >Password must be at least 6 characters</span
                  >
                </div>
              </div>
              <div class="form-group">
                <label for="confirmPassword">Confirm Password</label>
                <input
                  type="password"
                  v-model="user.confirmPassword"
                  id="confirmPassword"
                  name="confirmPassword"
                  class="form-control"
                  :class="{
                    'is-invalid': submitted && $v.user.confirmPassword.$error,
                  }"
                />
                <div
                  v-if="submitted && $v.user.confirmPassword.$error"
                  class="invalid-feedback"
                >
                  <span v-if="!$v.user.confirmPassword.required"
                    >Confirm Password is required</span
                  >
                  <span v-else-if="!$v.user.confirmPassword.sameAsPassword"
                    >Passwords must match</span
                  >
                </div>
              </div>
              <div class="form-group">
                <button class="btn btn-primary">Register</button>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { required, email, minLength, sameAs } from "vuelidate/lib/validators";

export default {
  name: "app",
  data() {
    return {
      user: {
        firstName: "",
        lastName: "",
        email: "",
        password: "",
        confirmPassword: "",
      },
      submitted: false,
    };
  },
  validations: {
    user: {
      firstName: { required },
      lastName: { required },
      email: { required, email },
      password: { required, minLength: minLength(6) },
      confirmPassword: { required, sameAsPassword: sameAs("password") },
    },
  },
  methods: {
    handleSubmit(e) {
      this.submitted = true;

      // stop here if form is invalid
      this.$v.$touch();
      if (this.$v.$invalid) {
        return;
      }

      alert("SUCCESS!! :-)\n\n" + JSON.stringify(this.user));
    },
  },
};
</script>

Lo primero que tienes que hacer es meter dentro de la lógica del componente un objeto llamado validations en el que tienes que escribir todas las propiedades que quieres validar.

Lo siguiente que tienes que saber es que Vuelidate expone un objeto llamado $v que contiene los errores de las validaciones. Además, este objeto contiene el objeto sobre el que tienes que hacer v-model en los inputs. Es decir tienes que hacer:

<input
  type="text"
  v-model="$v.user.firstName.$model"
  id="firstName"
  name="firstName"
  class="form-control"
/>

Otra forma de hacerlo, que es la que puedes ver en el ejemplo del formulario que estamos comentando, es crear un v-model con un objeto llamado igual que el que hay en las validaciones y con las mismas propiedades, es decir:

data() {
    return {
        user: {
            firstName: "",
            lastName: "",
            email: "",
            password: "",
            confirmPassword: ""
        },
        submitted: false
    };
},

Ahora los errores, como he dicho, dentro del objeto global $v tienes las validaciones. Por ejemplo para pintar el error del email:

<div v-if="submitted && $v.user.email.$error" class="invalid-feedback">
  <span v-if="!$v.user.email.required">Email is required</span>
  <span v-if="!$v.user.email.email">Email is invalid</span>
</div>

Además, tienes que poner el input con la clase error a mano también usando este mismo objeto.

Por último, dentro del botón de submit puedes comprobar si todos los campos son correctos mediante:

if (this.$v.$invalid) {
  return;
}

Como ves, esta librería es mucho más tediosa que vee-validate porque tienes que montar muchas cosas a mano.

DEMO: https://codesandbox.io/s/r0lkrn4wxo?file=/app/App.vue:4743-4828

Conclusiones

Otra forma de poder validar formularios, que no la he explicado para no hacer demasiado extenso este artículo, es la de instalar una librería completa de formularios. En lugar de tener que meter a mano los inputs, en este tipo de librerías tienes que crear un objeto de configuración con los campos que quieres en tu formulario y las validaciones que necesitas. Es otra alternativa también válida, aunque solo la recomiendo si tienes muchos formularios en la web.

Te dejo la librería de Vue-formulate por si quieres echar un ojo a este tipo de librerías:

https://vueformulate.com/

En general, yo recomiendo la librería de vee-validate siempre que sea posible ya que las abstracciones para los inputs permiten que el código sea menos complejo.

Si hay que validar otro tipo de datos aparte de inputs o hay que hacer cosas más complejas, si que puede interesar instalar la de Vuevalidate.