Валидация форм во Vue.js

в 6:30, , рубрики: javascript, vue, vue.js, vuejs, Блог компании Constanta, валидация данных, валидация форм, Программирование, Разработка веб-сайтов

Привет!

Валидация форм — одна из самых важных задач на сайте. Нам приходится валидировать данные на присутствие, на совпадение по паттерну, создавать асинхронные валидации, применять валидации только после снятия фокуса с поля или перед отправкой формы… Порой это становится настоящей головной болью для разработчика.

Vue.js содержит много интересных и необычных подходов к валидации, которые помогут решить ваши проблемы. Обзор под катом!

Валидация форм во Vue.js - 1

TL;DR

Используйте Vuelidate.

Частые ошибки

Валидации с помощью средств HTML5

HTML5 дал разработчикам возможность валидации форм с помощью новых атрибутов для полей и Validation API. Мы можем использовать их прямо в наших vue-шаблонах.

Вот, например, простая форма регистрации, состоящая из трех полей: поля для электронной почты, для пароля и повтора пароля. Для первых двух мы используем валидации с помощью атрибутов, для третьего — с помощью атрибутов и Validation API:

<template>
  <form @submit.prevent="someAction()">
    <input v-model="email" type="email" required>
    <input v-model="password" type="password" required>
    <input v-model="repeatedPassword" type="password" required ref="repeatedPasswordEl">

    <button type="submit">
      Отправить форму
    </button>
  </form>
</template>

<script>
export default {

  data() {
    return {
      email: null,
      password: null,
      repeatedPassword: null,
    };
  },

  watch: {
    repeatedPassword: 'checkPasswordsEquality',
    password: 'checkPasswordsEquality',
  },

  methods: {
    checkPasswordsEquality() {
      const { password, repeatedPassword } = this;
      const { repeatedPasswordEl } = this.$refs;

      if (password !== repeatedPassword) {
        repeatedPasswordEl.setCustomValidity(
          'Пароли должны совпадать',
        );
      } else {
        repeatedPasswordEl.setCustomValidity('');
      }
    },
  },

};
</script>

Песочница с примером

Даже в таком простом примере можно увидеть много проблем:

  • Браузеры показывают только одну ошибку за один раз. Пользователю придется пытаться отправить форму несколько раз, чтобы увидеть все свои ошибки.
  • Ошибки отображаются пользователю только после попытки отправки формы. Для изменения этого поведения придется написать еще тонну кода: вызывать у каждого элемента по событию blur функцию reportValidity().
  • Для стилизации полей ввода существуют только псевдоклассы :valid и :invalid, нет возможности поймать состояние, когда данные в поле ввода неверны, но пользователь еще не взаимодействовал с ним.
  • Каждый браузер отображает ошибки валидации по-своему, они могут выглядеть некрасиво в вашем дизайне.
  • Validation API во Vue.js неудобно использовать: приходится сохранять элементы в $refs.

Если у вас в проекте всего одна форма, и та используется только вами, то HTML5 валидации — отличный выбор. Во всех остальных остальных случаях старайтесь использовать другие подходы.

Валидации без использования библиотек

Другая частая проблема, которую я встречаю в проектах — валидации без использования библиотек.

<template>
  <form @submit.prevent="someAction()">
    <input
      v-model="email"
      type="email"
      @blur="isEmailTouched = true"
      :class="{ error: isEmailError }"
    >
    <div v-if="isEmailError">
      Поле заполено неверно
    </div>

    <button :disabled="!isEmailValid" type="submit">
      Отправить форму
    </button>
  </form>
</template>

<script>
const emailCheckRegex = /^...Длинный RegExp для валидации Email...$/;

export default {

  data() {
    return {
      email: null,
      isEmailTouched: false,
    };
  },

  computed: {
    isEmailValid() {
      return emailCheckRegex.test(this.email);
    },

    isEmailError() {
      return !this.isEmailValid && this.isEmailTouched;
    },
  },

};
</script>

Песочница с примером

В таком подходе нет проблем. За исключением того, что в формах с количеством полей больше одного рождается много повторяющегося кода, и каждый разработчик пишет валидации по-своему.

Лучший выход из всех проблем — пользоваться библиотеками, которые предлагает нам сообщество. На текущий момент есть два популярных решения для валидации форм:

Каждая имеет свой уникальный подход, которые мы подробно рассмотрим ниже.

vee-validate

vee-validate — библиотека для валидаций, появившаяся во времена Vue.js первой версии, имеет большое сообщество и используется в большом количестве проектов.

Возраст сказывается на весе — 31 КБайт (minified+GZIP), в полтора раза больше самого Vue.js! Связано это с тем, что библиотека содержит в себе сразу кучу вещей:

  • 35 встроенных валидаторов
  • Перевод ошибок на английский язык
  • Директивы для валидаций
  • Компоненты для валидаций

Библиотека поддерживает 41 язык для ошибок. Чтобы установить и использовать ее с нужной локализацией, требуется выполнить всего пару шагов:

npm i vee-validate

/* main.js */

import Vue from 'vue';
import VeeValidate, { Validator } from 'vee-validate';
import ru from 'vee-validate/dist/locale/ru';

Validator.localize('ru', ru);
Vue.use(VeeValidate, {
  locale: 'ru',
});

/* ... */

У vee-validate есть два подхода к валидациям: с помощью директивы и с помощью компонентов.

Валидация с помощью директивы v-validate

Подход с помощью директивы очень прост: вы вешаете на поле ввода директиву v-validate, которой передаете список валидаторов. Состояние валидации и строчки ошибок затем можно получить из полей fields и errors внутри компонента.

Чтобы посмотреть ее использование на примере, давайте сделаем простую форму, которая:

— Содержит поле «Серия и номер паспорта»
— Содержит поле «Дата выдачи паспорта»
— Содержит поле «Имя»
— Добавляет атрибут disabled на кнопку отправки формы если данные неверны

<template>
  <form @submit.prevent="someAction()">
    <div>
      <!--
        Ошибка в поле будет отображена сразу после начала ввода в него данных

        data-vv-as - атрибут для кастомизации названия поля в строчке с ошибкой
        name - название поля внутри объекта валидатора
      -->
      <input
        type="text"
        v-model="passportData"
        v-validate="{ required: true, regex: /^d{4} d{6}$/ }"
        data-vv-as="серия и номер паспорта"
        name="passport_data"
      >
      <span v-if="errors.has('passport_data')">
        {{ errors.first('passport_data') }}
      </span>
    </div>

    <div>
      <!--
        По дефолту все ошибки отобразятся как только вы начнете вводить данные.
        Для изменения этого поведения используйте v-model-модификатор lazy
        или флаги валидации внутри fields.passport_date

        Флаг invalid означает то, что данные в поле неправильные
        Флаг touched означает, что поле ввода создало событие blur
      -->
      <input
        type="text"
        v-model.lazy="passportDate"
        v-validate="{ required: true, date_format: 'dd.MM.yyyy' }"
        data-vv-as="дата выдачи паспорта"
        name="passport_date"
      >
      <span 
        v-if="fields.passport_date 
          && fields.passport_date.touched 
          && fields.passport_date.invalid"
      >
        {{ errors.first('passport_date') }}
      </span>
    </div>

    <div>
      <!--
        Если хотите отобразить сразу все ошибки поля,
        то используйте модификатор continues и метод errors.collect()
      -->
      <input
        type="text"
        v-model="name"
        v-validate.continues="{ required: true, alpha: true, max: 10 }"
        data-vv-as="имя"
        name="name"
      >
      <span
        v-for="error in errors.collect('name')"
        :key="error"
      >
        {{ error }}
      </span>
    </div>

    <button 
      type="submit"
      :disabled="!isFormValid"
    >
      Отправить форму
    </button>
  </form>
</template>

<script>
export default {
  data() {
    return {
      passportData: null,
      name: null,
      passportDate: null,
    };
  },

  computed: {
    // Проверяем, что каждое поле формы валидно
    isFormValid () {
      return Object.keys(this.fields).every(field => this.fields[field].valid);
    },
  },
};
</script>

Песочница с примером

Как можно было заметить, при использовании флагов внутри fields.passport_date пришлось проверить поле passport_date на присутствие — во время первого рендера у vee-validate нет информации о ваших полях и, соответственно, нет объекта с флагами. Чтобы избавиться от этой проверки, используйте специальный хелпер mapFields.

Валидации с помощью компонентов

Новый способ валидации, который появился в конце прошлого года — использование компонентов. Сами авторы рекомендуют использовать этот подход, и он отлично сочетается с новым синтаксисом слотов из Vue.js@2.6.

Библиотека предоставляет два компонента:

  • ValidationProvider — компонент, в котором вы описываете валидации и в который оборачиваете поле ввода.
  • ValidationObserver — компонент, который объединяет флаги валидаций нескольких ValidationProvider'ов.

Вот форма из прошлого примера, но написанная с помощью компонентов:

<template>
  <!--
    ValidationObserver передает дефолтному слоту все флаги валидаций.
    В примере используется только флаг valid для стилизации кнопки отправки формы
  -->
  <ValidationObserver v-slot="{ valid }">
    <form @submit.prevent="doAction()">
      <!--
        ValidationProvider добавляет обработчики на все поля ввода внутри слота
        и передает слоту все ошибки и флаги валидации

        name - атрибут, который будет отображаться в ошибках
      -->
      <ValidationProvider
        name="серия и номер паспорта"
        :rules="{ required: true, regex: /^d{4} d{6}$/ }"
        v-slot="{ errors }"
      >
        <input type="text" v-model="passportData">
        <span v-if="errors[0]">
          {{ errors[0] }}
        </span>
      </ValidationProvider>

      <!--
        По дефолту валидация данных сработает сразу после события input,
        для валидации после blur или change используйте mode="lazy" или mode="eager"
      -->
      <ValidationProvider
        name="дата выдачи паспорта"
        :rules="{ required: true, date_format: 'dd.MM.yyyy' }"
        v-slot="{ errors }"
        mode="lazy"
      >
        <input type="text" v-model="passportDate">
        <span v-if="errors[0]">
          {{ errors[0] }}
        </span>
      </ValidationProvider>

      <!--
        Если хотите отобразить сразу все ошибки поля,
        то используйте входной параметр :bails="false"
      -->
      <ValidationProvider
        name="имя"
        :rules="{ required: true, alpha: true, max: 10 }"
        v-slot="{ errors }"
        :bails="false"
      >
        <input type="text" v-model="name">
        <span
          v-for="error in errors"
          :key="error"
        >
          {{ error }}
        </span>
      </ValidationProvider>

      <button type="submit" :disabled="!valid">
        Отправить форму
      </button>
    </form>
  </ValidationObserver>
</template>

<script>
import { ValidationProvider, ValidationObserver } from 'vee-validate';

export default {
  components: {
    ValidationProvider,
    ValidationObserver,
  },

  data() {
    return {
      passportData: null,
      passportDate: null,
      name: null,
    };
  },
};
</script>

Песочница с примером

Остальные возможности библиотеки

Проблемы

Первая и самая большая проблема — это, конечно же, ее размер. Библиотека не поддерживает tree-shaking и выборочное добавление валидаторов. Хотите вы или нет, в вашем банде всегда будет присутствовать 2 компонента и директива валидаций, перевод на английский язык и 35 валидаторов.

Вторая проблема — из-за подхода, основанного на подписке на события, могут возникать проблемы при интеграции с другими библиотеками, которые тоже изменяют поведение полей ввода (маскеры и т.п.).

Третья проблема — более субъективная — переводы ошибок общие и некрасивые, не отражают реальной сути.

Возьмем форму из прошлых примеров. Если вы введете неправильный номер и дату паспорта, то получите такие ошибки:

Поле серия и номер паспорта имеет ошибочный формат.
Поле дата выдачи паспорта должно быть в формате dd.MM.yyyy.

Хочется заменить их на что-то более удобочитаемое:

Поле серия и номер паспорта должно быть в формате 1234 567890
Поле дата выдачи паспорта должно быть в формате ДД.ММ.ГГГГ

Vuelidate

Библиотека Vuelidate появилась в ответ на проблемы с подходами, которые содержит в себе библиотека vee-validate. Vuelidate не имеет ни обработчиков событий для полей, ни переводов для ошибок валидации.

Она требует от вас только одного — описать валидации в объекте validations. Затем она сама создаст computed-поле $v с флагами валидации полей и функциями для изменения этих флагов.

Благодаря простому подходу к валидациям, Vuelidate весит всего 3.4 КБайта (minified+GZIP), почти в 10 раз меньше vee-validate.

Устанавливается она так же просто:

npm i vuelidate

/* main.js */

import Vue from 'vue'
import Vuelidate from 'vuelidate'
Vue.use(Vuelidate)

/* ... */

Перепишем форму из прошлого примера с использованием Vuelidate:

<template>
  <form @submit.prevent="someAction()">
    <!--
      Ошибка будет отображена пользователю сразу же,
      флаг $v.passportData.$invalid говорит о том, валидное поле или нет
    -->
    <div>
      <input type="text" v-model="passportData">
      <span v-if="$v.passportData.$invalid">
        Серия и номер паспорта должны быть в формате 1234 567890
      </span>
    </div>

    <!--
      Ошибка будет отображена после события blur

      Метод $touch() выставит флагу $v.passportDate.$dirty значение true.
      Флаг $v.passportDate.$error высчитывается как
      $v.passportDate.$invalid && $v.passportDate.$dirty
    -->
    <div>
      <input type="text" v-model="passportDate" @blur="$v.passportDate.$touch()">
      <span v-if="$v.passportDate.$error">
        Дата должна быть в формате ДД.ММ.ГГГГ
      </span>
    </div>


    <!--
      Поле, которое тоже выведет ошибку после события blur, но с другим подходом

      $v.passportDate.$model - объект, при записи данных в который:
      - Vuelidate присвоит переданное значение полю passportDate
      - Vuelidate вызовет метод $touch() у объекта $v.passportDate

      Модификатор lazy необходим, чтобы присваивание произошло только после blur
    -->
    <div>
      <input type="text" v-model.lazy="$v.passportDate.$model">
      <span v-if="$v.passportDate.$error">
        Дата должна быть в формате ДД.ММ.ГГГГ
      </span>
    </div>

    <!-- Поле с несколькими ошибками -->
    <div>
      <input type="text" v-model="name" @blur="$v.name.$touch()">
      <span v-if="$v.name.$error">
        <template v-if="!$v.name.maxLength">
          Длина имени не должна превышать {{ $v.name.$params.maxLength.max }} символов
        </template>
        <template v-else-if="!$v.name.alpha">
          Имя должно содержать только буквы
        </template>
        <template v-else>
          Имя обязательно для заполнения
        </template>
      </span>
    </div>

    <button type="submit" :disabled="$v.$invalid">
      Отправить форму
    </button>
  </form>
</template>

<script>
import { required, maxLength } from 'vuelidate/lib/validators';
import moment from 'moment';

export default {
  data() {
    return {
      passportData: null,
      name: null,
      passportDate: null,
    };
  },

  // Модель для валидации, которую Vuelidate превратит в computed-поле $v
  validations: {
    // Название поля должно совпадать с полем в data
    passportData: {
      required,
      validFormat: val => /^d{4} d{6}$/.test(val),
    },
    passportDate: {
      required,
      validDate: val => moment(val, 'DD.MM.YYYY', true).isValid(),
    },
    name: {
      required,
      maxLength: maxLength(10),
      alpha: val => /^[а-яё]*$/i.test(val),
    },
  },
};
</script>

Песочница с примером

Кастомные валидаторы можно быстро и легко описывать с помощью функций. Если вы хотите, чтобы параметры вашего валидатора попали в объект $params, используйте специальный хелпер withParams.

Как можно заметить, поля с несколькими ошибками занимают очень много места и выглядят монструозно — решается это созданием отдельного компонента для отображения валидаций.

Остальные возможности библиотеки

Проблемы

Из проблем можно выделить разве что сравнительно скудное количество валидаторов из коробки, зачастую приходится писать свои функции для валидации.

Заключение

Библиотеку Vuelidate я использовал во многих проектах и ни разу не сталкивался с нерешаемыми проблемами. Для меня она остается лучшим выбором. Если вы заботитесь о размере своего кода и предпочитаете модельный подход к описанию валидаций — берите ее, простая документация и богатое апи позволит валидировать формы любой сложности.

Если вы делаете админку для внутреннего использования, не хотите тратить ни капли времени на строчки ошибок — выбирайте vee-validate. Она поможет быстро и без проблем написать много валидаций.

Спасибо за внимание!

Автор: AndreasCag

Источник

* - обязательные к заполнению поля