Как стать автором
Обновить

Валидация форм через CSS, RegExp и компоненты TS

Всем привет! Проблема валидации форм стара и затерта до дыр. Тем не менее, я думаю не будет лишним продемонстривать мой путь решения этой задачи. Кроме того, подход, изложенный ниже, является одним из самых изящных и лаконичных на мой скромный вкус. Там где вы можете справиться без TS/JS, используя лишь CSS и HTML, предпочитайте такой путь, поскольку он делегирует решение проблемы напрямую в браузер, а значит вы выигрываете в оптимизации вашего приложения.

Задача


У нас есть стандартная форма регистрации. Нам необходимо провалидировать каждое поле по особым правилам: чтобы почтовый адрес соответсвовал стандартному email-скелету, чтобы имя и фамилия начинались с большой буквы и тд. Я не буду расписывать каждое поле в статье, но подробно остановлюсь на двух последних - "пароль" и "повтор пароля". Почему так? Поле "пароль" - стандартное, провалидиров его, я покажу процесс валидации любого другого поля данной формы. А вот поле с повтором пароля особенное, правило валидации у него динамически меняется в зависимости от того, что введено в поле "пароль".

Решение

Стоит оговориться, что в моем приложении компонентный подход релизован своими силами, но если вы работает с React, Angular или Vue, суть вам будет понятна. Вот что представляет из себя шаблон формы:

<form class="form reg">
        <div class="head">
            <div class="name">Регистрация</div>
            <div class="inputs">
                <div class="tip">Почта</div>
                {{{ inputEmail }}}
                <div class="error">Укажите верный адрес</div>

                <div class="tip">Логин</div>
                {{{ inputLogin }}}
                <div class="error">Неверный логин</div>

                <div class="tip">Имя</div>
                {{{ inputFirstName }}}
                <div class="error">Укажите имя</div>

                <div class="tip">Фамилия</div>
                {{{ inputSecondName }}}
                <div class="error">Укажите фамилию</div>

                <div class="tip">Телефон</div>
                {{{ inputPhone }}}
                <div class="error">Укажите верный номер</div>

                <div class="tip">Пароль</div>
                {{{ inputPassword }}}
                <div class="error">Пароль введен неправильно</div>

                <div class="tip">Пароль (еще раз)</div>
                {{{ inputPasswordCheck }}}
                <div class="error">Пароли не совпадают</div>
            </div>
        </div>
        <div class="footer">
            {{{ button }}}
            <a href="index.html">
                <div class="ask">Войти</div>
            </a>
        </div>
    </form>

Нас интересуют компоненты inputPassword и inputPasswordCheck. Это самые обыкновенные инпуты, которые принимают ряд параметров для своих атрибутов. Во фреймворках эти параметры передаются прямо через верстку, но т.к. мой вариант компонентов самописный, я передаю их через TS. Вот шаблон компонента инпута, хорошо видно какие параметры он может принять:

<input 
    class="inner"
    autocomplete="off"
    type="{{type}}"
    name="{{name}}"
    minlength="{{minlength}}"
    maxlength="{{maxlength}}"
    pattern="{{pattern}}"
    placeholder=" "
    required
/>

Итак давайте провалидируем инпут пароля за который отвечает компонент inputPassword. Правило такое: "от 8 до 40 символов, обязательно хотя бы одна заглавная буква и цифра". Чтож, тут нам потребуется прибугнуть к регулярному выражению, не самое приятное занятие на мой вкус, но со скрипом и гуглом была подготовлена такая строка, которую мы для удобства упаковали в enum ValidateRules с нашими правилами.

^(?=.*[A-Z])(?=.*[0-9])[-_A-Za-z0-9]{8,40}$

Благолополучно оборачиваем ее классом RegExp и дело в шляпе. Подаем данное правило в наш компонент, чтобы тот подставил регулярное выражение в атрибут инпута [pattern].

  this.children.inputPassword = new Input({
      name: 'password',
      type: 'password',
      pattern: ValidateRules.password,
  });

Почти готово. Инпут уже валидирует свое содержимое, осталось сообщить пользователю. Тут нам пригодится CSS. Давайте попросим браузер следить - если инпут не в фокусе и при этом не проходит правило валидации, тогда наш div с классом error (в котором содержится текст ошибки) пусть явит себя пользователю.

  input:not(:focus):invalid + .error {
      display: block;
  }

Изящно! Но есть нюанс :) Совсем без ts обойтись не выйдет. Все дело в том, что проклятые хакеры могут воспользоваться секретным оружием пентагона (DevTools браузера) и изменить правила валидации, посколько это лишь атрибут инпута. Тогда форма пропустит невалидный submit и мы будем разочарованы. Поэтому грамотным решением будет вторичная валидация всех инпутов при помощи готовых регулярных выражений уже "засабмиченой" формы. Это все еще весьма изящно, т.к. мы избежали навешивания событий на КАЖДЫЙ инпут, мы (не дай бог) не анализировали value инпутов в циклах сложностью Θ(n²) и вообще избежали кучи проблем. Пример кода я покажу в следующем блоке статьи.

Динамическая валидация

А что делать с инпутом проверки пароля? Нам нужно проверять поле повторного ввода на равенство со значением инпута пароля. Я решил этот момент так - повесил на инпут оригинального пароля событие ввода, которое при каждом новом вводе формирует регулярное выражение для сравнения первого и повторного ввода пароля и обновлет его как паттерн инпута passwordCheck-a. А уже дальше работает магия HTML и СSS которые проверят и сообщат пользователю, если он налажал.

this.children.inputPassword = new Input({
  name: 'password',
  type: 'password',
  pattern: ValidateRules.password,
  events: {
    input: () => {
      const passwordElement = <HTMLInputElement> this.children.inputPassword.element;
      this.children.inputPasswordCheck.setProps({
        pattern: '^' + passwordElement.value + '$',
      });
    },
  },
});
this.children.inputPasswordCheck = new Input({
  name: 'passwordCheck',
  type: 'password',
  pattern: ValidateRules.password,
});

Ну и как и обещал кусочек кода, где я осуществляю вторичную валидацию уже в отработавшем событии формы submit. Просто проверю результат еще раз на готовые ругулярки и вывожу в консоль лог || отправляю на сервер || etc.

this.setProps({
  events: {
    submit: (e: SubmitEvent) => {
      e.preventDefault();
      const data = [...new FormData(e.target as HTMLFormElement)];
      const entries = new Map(data.slice(0, -1));
      const result = Object.fromEntries(entries);
      const checkEmail = this._emailRule.test(data[0][1].toString());
      const checkLogin = this._loginRule.test(data[1][1].toString());
      const checkFirstName = this._nameRule.test(data[2][1].toString());
      const checkSecondName = this._nameRule.test(data[3][1].toString());
      const checkPhone = this._phoneRule.test(data[4][1].toString());
      const checkPassword = this._passwordRule.test(data[5][1].toString());
      const checkPasswordAgain = data[5][1].toString() === data[6][1].toString();
      if (checkEmail &&
          checkLogin &&
          checkFirstName &&
          checkSecondName &&
          checkPhone &&
          checkPassword &&
          checkPasswordAgain
      ) {
          console.log(result);
      }
    },
  },
});

Ну и давайте напоследок бросим взгляд на то, как это работает. По-моему, мило.



Всем спасибо! Это была моя первая статья на Хабр. Случай хоть и банальный, но в деталях показался мне интересным.

Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.