Доброго времени суток, друзья!

Представляю вашему вниманию небольшой проект, представляющий собой реализацию полноценной валидации формы. Слово «полноценная» означает, что проверка корректности введенных пользователем данных осуществляется средствами HTML, клиентского и серверного JavaScript.

Превью проекта:



Как вы можете видеть, форма включает следующие поля:

  • Имя
  • Возраст
  • Телефон
  • Пароль
  • Адрес электронной почты
  • Адрес сайта
  • Номер кредитной карты
  • Дата
  • Время

Также в форме имеется три кнопки: для отправки данных на сервер, очистки полей формы и заполнения полей и объекта, содержащего пользовательские данные, фиктивными данными. Последняя кнопка обеспечивает возможность быстрой отправки данных на сервер для тестирования его работоспособности.

Код проекта находится здесь.

Демо приложения можно посмотреть здесь.

Структура проекта выглядит следующим образом:

form-validation-example
  components
    check-field.js
    filter-field.js
    main-helpers.js
    second-helpers.js
    validator.js
  server
    index.html
    index.js
  src
    script.js
    style.css
  index.html

Кратко разберем назначение и содержимое каждого из этих файлов. Начнем с index.html из корневой директории.

Оглавление:



Валидация формы средствами HTML


Для стилизации формы используется Bootstrap. Подключаем его в head:

<link
  rel="stylesheet"
  href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
  integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z"
  crossorigin="anonymous"
/>

Для валидации полей формы используется несколько специальных атрибутов. Основными из них являются следующие:

  • type — определяет тип поля, например, в поле с атрибутом «type='number'» можно вводить только цифры (в форме используются такие типы, как name, number, tel, password, email, url, date и range)
  • required — означает, что поле является обязательным для заполнения
  • minlength — определяет минимальную длину строки (количество символов)
  • maxlength — определяет максимальную длину строки
  • min — определяет минимально допустимое число (значение)
  • max — определяет максимально допустимое значение
  • step — определяет шаг (величину изменения значения)

Атрибут «placeholder» используется в качестве подсказки для правильного заполнения поля. Атрибут «value» позволяет устанавливать значение поля по умолчанию. Этот атрибут используется в скрипте для заполнения формы фиктивными данными.

Обратите внимание, что атрибут «id» (идентификатор), кроме того, что должен являться уникальным, т.е. присутствовать на странице в единственном экземпляре, а также, кроме связывания инпута с label (в label для указания принадлежности к конкретному инпуту используется атрибут «for»), позволяет напрямую обращаться к элементу в скрипте (например, к элементу с идентификатором «name» можно обращаться просто как к name без предварительного определения элемента с помощью querySelector или аналогичных методов).

Полный код разметки:
<h1 class="title">Валидация формы</h1>
<form>
  <!-- name -->
  <div class="form-group row">
  <label for="name" class="col-sm-2 col-form-label">Имя</label>
  <div class="col-sm-10">
    <input
      type="text"
      class="form-control"
      id="name"
      placeholder="Игорь"
      minlength="2"
      maxlength="20"
      required
    /></div>
  </div>

  <!-- age -->
  <div class="form-group row">
  <label for="age" class="col-sm-2 col-form-label">Возраст</label>
  <div class="col-sm-10">
    <input
      type="number"
      class="form-control"
      id="age"
      placeholder="30"
      min="18"
      max="100"
      step="1"
      required
    /></div>
  </div>

  <!-- phone -->
  <div class="form-group row">
  <label for="phone" class="col-sm-2 col-form-label">Телефон</label>
  <div class="col-sm-10">
    <input
      type="tel"
      class="form-control"
      id="phone"
      placeholder="+79876543210"
      required
    /></div>
  </div>

  <!-- password -->
  <div class="form-group row">
  <label for="password" class="col-sm-2 col-form-label">Пароль</label>
  <div class="col-sm-10">
    <input
      type="password"
      class="form-control"
      id="password"
      placeholder="p@sSw0rd"
      minlength="8"
      maxlength="12"
      required
    /></div>
  </div>

  <!-- email -->
  <div class="form-group row">
  <label for="email" class="col-sm-2 col-form-label">Email</label>
  <div class="col-sm-10">
    <input
      type="email"
      class="form-control"
      id="email"
      placeholder="email@example.com"
      required
    /></div>
  </div>

  <!-- site -->
  <div class="form-group row">
  <label for="url" class="col-sm-2 col-form-label">Сайт</label>
  <div class="col-sm-10">
    <input
      type="url"
      class="form-control"
      id="url"
      placeholder="https://whatever.org"
      required
    /></div>
  </div>

  <!-- card -->
  <div class="form-group row">
  <label for="card" class="col-sm-2 col-form-label">Карта</label>
  <div class="col-sm-10">
    <input
      type="text"
      class="form-control"
      id="card"
      placeholder="1111222233334444"
      minlength="16"
      maxlength="16"
      required
    /></div>
  </div>

  <!-- date -->
  <div class="form-group row">
  <label for="date" class="col-sm-2 col-form-label">Дата</label>
  <div class="col-sm-10">
    <input
      type="date"
      class="form-control"
      id="date"
      required /></div>
  </div>

  <!-- time -->
  <div class="form-group row">
  <label for="time" class="col-sm-2 col-form-label">Время</label>
  <div class="col-sm-10">
    <span id="tooltip">10:00-12:00</span>
    <input
      type="range"
      class="form-control"
      id="time"
      min="10"
      max="18"
      step="0.5"
      value="10"
      required/></div>
</div>

  <div class="form-group row">
  <div class="col-8">
    <button
      type="submit"
      class="btn btn-primary"
      id="send"
      disabled
    >
      Отправить
    </button>
    <button
      type="reset"
      class="btn btn-warning"
    >
      Очистить
    </button>
    <button
      type="button"
      class="btn btn-success"
      id="set"
    >
      Данные
    </button>
  </div>
  </div>
</form>


На стилях останавливаться не будем, в них нет ничего особенного.

Валидация формы средствами клиенсткого JavaScript


Безусловно, сердцем валидации данных являются регулярные выражения. Некоторые выражения позаимствованы мной со Stack Overflow, другие написаны самостоятельно, учитывая специфику проводимых проверок.

Вот что мы хотим получить от пользователя:

  • Имя, набранное русскими буквами, минимальная длина — 2, максимальная — 10 (просто ради примера)
  • Возраст в диапазоне от 18 до 100 лет
  • Номер телефона, который должен начинаться с "+7"
  • Пароль от 8 до 12 символов, содержащий минимум одну заглавную букву, одну строчную букву, одну цифру и один специальный символ
  • Адрес электронной почты
  • Адрес сайта с обязательным указанием протокола
  • Номер кредитной карты
  • Дату в диапазоне «завтрашний день — через неделю» в формате «DD.MM.YYYY»
  • Время в диапазоне 10-18 в формате «10:00-12:00» (разница — 2 часа, шаг — 30 минут)

Далее я приведу код скриптов и кратко его прокомментирую.

Файл «script.js» представляет собой основной файл приложения, который подключается в index.html (не забудьте указать атрибут «type» со значением «module»):

// импортируем основные вспомогательные функции, функции проверки полей формы и фильтрации ввода пользователя
import { mainHelpers } from "../components/main-helpers.js";
import { checkField } from "../components/check-field.js";
import { filterField } from "../components/filter-field.js";

// поскольку каждый модуль, включая script.js, имеет собственную область видимости
// объявленные в нем на верхнем уровне переменные не являются глобальными
// т.е. недоступны для других модулей
// поэтому определяем глобальные переменные для элемента формы и объекта с пользовательскими данными явно
window.form = document.querySelector("form");
window.userData = {};

// получаем необходимые вспомогательные функции
const {
  setMinAndMaxDates,
  changeTimeTooltip,
  checkUserData,
  sendUserData,
  setDummyData,
} = mainHelpers();

// получаем функции проверки полей формы
const {
  checkName,
  checkAge,
  checkPhone,
  checkPassword,
  checkEmail,
  checkUrl,
  checkCard,
  checkDate,
  checkTime,
} = checkField();

// получаем функции фильтрации пользовательского ввода
const {
  filterName,
  filterAge,
  filterPhone,
  filterCard
} = filterField();

// основные настройки приложения
const mainSetup = () => {
  // добавляем обработчик нажатия клавиши клавиатуры
  form.addEventListener("keypress", (e) => {
    if (e.target.tagName !== "INPUT") return;

    const type = e.target.id;

    // вызываем соответствующую функцию фильтрации пользовательского ввода в зависимости от инпута
    switch (type) {
      case "name":
        filterName(e);
        break;
      case "age":
        filterAge(e);
        break;
      case "phone":
        filterPhone(e);
        break;
      case "card":
        filterCard(e);
        break;
    }
  })

  // добавляем обработчик изменения значения поля формы
  form.addEventListener("change", (e) => {
    if (e.target.tagName !== "INPUT") return;

    const type = e.target.id;
    const { value } = e.target;

    // вызываем соответствующую функцию проверки поля формы в зависимости от инпута, передавая его значение в качестве аргумента
    switch (type) {
      case "name":
        checkName(value);
        break;
      case "age":
        checkAge(value);
        break;
      case "phone":
        checkPhone(value);
        break;
      case "password":
        checkPassword(value);
        break;
      case "email":
        checkEmail(value);
        break;
      case "url":
        checkUrl(value);
        break;
      case "card":
        checkCard(value);
        break;
      case "date":
        checkDate(value);
        break;
      case "time":
        checkTime(value);
        break;
    }

    console.log(userData);
    // вызываем функцию проверки данных пользователя для определения того,
    // все ли поля заполнены
    checkUserData(userData);
  })

  // отключаем обработку отправки формы браузером по умолчанию
  form.addEventListener("submit", (e) => e.preventDefault())

  // добавляем обработчик нажатия кнопки для отправки данных на сервер
  send.addEventListener("click", () => sendUserData(userData))

  // дополнительные настройки
  secondSetup()
}

const secondSetup = () => {
  // добавляем обработчик ввода данных в поле "time"
  time.addEventListener("input", (e) => changeTimeTooltip(e.target.valueAsNumber))

  // добавляем обработчик нажатия кнопки для заполнения полей формы и объекта с пользовательскими данными фиктивными данными
  set.addEventListener("click", setDummyData)

  // вызываем функцию установки минимальной и максимальной дат
  setMinAndMaxDates()
}

mainSetup()

В файле «second-helpers.js» содержатся второстепенные функции, которые используются в основных вспомогательных функциях:

// secondHelpers представляет собой фабричную функцию, возвращающую объект с определенными в ней функциями
export const secondHelpers = () => {
  // функция отображения сообщения об ошибках, принимающая массив с ошибками
  // в том числе, от сервера
  const showErrors = (errors) => {
    const template = `
<div class="alert alert-danger">
  ${errors.reduce((html, error) => (html += `<span>${error}</span><br>`), "")}
</div>
`;

    form.insertAdjacentHTML("beforeend", template);

    const timer = setTimeout(() => {
      form.querySelector(".alert").remove();
      clearTimeout(timer);
    }, 3000);
  };

  // функция отображения сообщения об успешной отправке данных на сервер
  const showSuccess = (value) => {
    const template = `
<div class="alert alert-success">
  <span>${value}</span>
</div>
`;

    form.insertAdjacentHTML("beforeend", template);

    const timer = setTimeout(() => {
      form.querySelector(".alert").remove();
      clearTimeout(timer);
    }, 3000);
  };

  // функция проверки номера кредитной карты с помощью алгоритма Луна
  // здесь используется моя реализация упрощенного варианта алгоритма
  //https://ru.wikipedia.org/wiki/Алгоритм_Луна#Упрощённый_алгоритм
  const luhnAlgorithm = (value) => {
    let len = value.length;
    let sum = 0;

    for (let num of value) {
      num = +num;

      if (len % 2 === 0) {
        num *= 2;

        if (num > 9) {
          num -= 9;
        }
      }

      sum += +num;
      len--;
    }

    return sum % 10 === 0;
  };

  // функция форматирования даты
  // инпут с типом "date" возвращает и принимает значения в формате "YYYY-MM-DD"
  // нам необходимо преобразовывать эти значения в формат "DD.MM.YYYY" и обратно
  const formatDate = (date, lang) => {
    if (lang === "en") {
      const regex = /(?<day>[0-9]{2}).(?<month>[0-9]{2}).(?<year>[0-9]{4})/;
      date = date
        .toLocaleDateString()
        .replace(regex, "$<year>-$<month>-$<day>");
    } else {
      const regex = /(?<year>[0-9]{4})-(?<month>[0-9]{2}).(?<day>[0-9]{2})/;
        date = date
        .toLocaleDateString()
        .replace(regex, "$<day>.$<month>.$<year>");
    }

    return date;
  };

  // функция форматирования времени
  // инпут с типом "range" возвращает значения в виде целых чисел или чисел двойной точности с одной цифрой после точки
  // нас интересует формат "10:00"
  // причина, по которой для выбора времени не используется инпут с типом "time"
  // состоит в отсутствии возможности ограничения вводимых данных
  const formatTime = (value) => {
    value = Number(value).toFixed(2).replace(/\./, ":").replace(/50/, "30");

    return value;
  };

  return {
    showErrors,
    showSuccess,
    luhnAlgorithm,
    formatDate,
    formatTime,
  };
};

В файле «main-helpers.js» содержатся основные вспомогательные функции:

// импортируем фабричную функцию, возвращающую дополнительные функции
import { secondHelpers } from "./second-helpers.js";
// получаем необходимые дополнительные функции
const { showErrors, showSuccess, formatDate, formatTime } = secondHelpers();

// mainHelpers также представляет собой фабричную функцию, возвращающую объект с определенными в ней функциями
export const mainHelpers = () => {
  // функция установки минимально и максимально допустимой даты
  const setMinAndMaxDates = () => {
    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);

    const afterWeek = new Date();
    afterWeek.setDate(afterWeek.getDate() + 8);

    date.setAttribute("min", formatDate(tomorrow, "en"));
    date.setAttribute("max", formatDate(afterWeek, "en"));
  };

  // функция изменения указателя выбранного времени
  // нас интересует формат "10:00-12:00", выбранное время + 2 часа, шаг - 30 минут
  // для тех, кого интересует, как сделать так, чтобы указатель выбранного времени следовал за "рэнджем"
  // https://css-tricks.com/value-bubbles-for-range-inputs/
  const changeTimeTooltip = (value) => {
    const firstValue = value;
    const secondValue = firstValue + 2;

    const timeValue = `${formatTime(firstValue)}-${formatTime(secondValue)}`;

    tooltip.textContent = timeValue;
  };

  // функция проверки пользовательских данных
  // в данной функции мы сначала проверяем, что длина ключает составляет 9 (по количеству инпутов)
  // затем проверяем, что ни одно из значений не является пустой строкой (т.е. ложным значением)
  // если оба условия удовлетворяются, разблокируем кнопку для отправки данных на сервер
  const checkUserData = (data) => {
    if (
      Object.keys(data).length === 9 &&
      Object.values(data).every((item) => !!(item.trim()) === true)
    ) {
      send.disabled = false;
    }
  };

  // фукнция отправки данных на сервер
  // данные отправляются на сервер, размещенный на Heroku
  // (о том, как это сделать будет рассказано в разделе про деплой приложения)
  // в процессе разработки приложения в качестве адреса сервера можно использовать http://localhost:1234/server или http://127.0.0.1:1234/server
  const sendUserData = async (data) => {
    send.disabled = true;

    const response = await fetch(
      "https://form-validation-server.herokuapp.com/server",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
      }
    );
    const result = await response.json();

    // если типом ответа от сервера является строка
    // значит, данные прошли валидацию
    // в противном случае, сервер возвращает массив с ошибками
    if (typeof result === "string") {
      showSuccess(result);
      send.disabled = false;
    } else {
      showErrors(result);
    }
  };

  // функция заполнения полей формы и объекта с пользовательскими данными фиктивными данными
  // обратите внимание, что номер карты 2222 1111 1111 1111 является валидным
  const setDummyData = () => {
    const inputs = form.querySelectorAll("input");

    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);

    const values = [
      "Игорь",
      "30",
      "+79876543210",
      "p@s$Wor6",
      "email@example.com",
      "https://whatever.org",
      "2222111111111111",
      formatDate(tomorrow, "en"),
      "10",
    ];

    inputs.forEach((input, index) => (input.value = values[index]));

    userData = {
      name: "Игорь",
      age: "30",
      phone: "+79876543210",
      password: "p@s$Wor6",
      email: "email@example.com",
      url: "https://whatever.org",
      card: "2222111111111111",
      date: formatDate(tomorrow, "ru"),
      time: "10:00-12:00",
    };

    console.log(userData);
    // вызываем функцию, чтобы разблокировать кнопку для отправки данных на сервер
    checkUserData(userData);
  };

  return {
    setMinAndMaxDates,
    changeTimeTooltip,
    checkUserData,
    sendUserData,
    setDummyData,
  };
};

В файле «filter-field.js» содержатся функции фильтрации пользовательского ввода:

// фабричная функция
export const filterField = () => {
  // имя
  // можно вводаить только русские буквы, независимо от регистра
  // в количестве не более 10
  const filterName = (e) => {
    const regex = /[а-яё]/i;
    if (!regex.test(e.key) || e.target.value.length > 9) {
      e.preventDefault();
    }
  };

  // возраст
  // можно вводить только цифры
  // в количестве не более 3
  const filterAge = (e) => {
    const regex = /\d/;
    if (!regex.test(e.key) || e.target.value.length > 2) {
      e.preventDefault();
    }
  };

  // номер телефона
  // можно вводить только символ "+" и цифры
  // в количестве не более 12
  const filterPhone = (e) => {
    const regex = /[+\d]/;
    if (!regex.test(e.key) || e.target.value.length > 11) {
      e.preventDefault();
    }
  };

  // номер кредитной карты
  // можно вводить только цифры
  // в количестве не более 16
  const filterCard = (e) => {
    const regex = /\d/;
    if (!regex.test(e.key) || e.target.value.length > 15) {
      e.preventDefault();
    }
  };

  return {
    filterName,
    filterAge,
    filterPhone,
    filterCard,
  };
};

В файле «check-field.js» содержатся функции проверки корректности введенных пользователем данных:

// импортируем валидатор и вспомогательные функции
import { validator } from "./validator.js";
import { secondHelpers } from "./second-helpers.js";

// получаем необходимые вспомогательные функции
const { showErrors, formatDate, formatTime } = secondHelpers();

// фабричная функция
export const checkField = () => {
  // имя
  // функция принимает значение соответствующего инпута
  // и проверяет его с помощью валидатора
  const checkName = (value) => {
    // получаем массив с ошибками
    const { errors } = validator(value).notEmpty().isAlpha()

    // если массив с ошибками не является пустым
    // отображаем сообщение с ошибками
    // иначе, записываем значение в объект с пользовательскими данными
    errors.length
      ? showErrors(errors)
      : (userData.name = `${value[0].toUpperCase()}${value
          .slice(1)
          .toLowerCase()}`);
  };

  // возраст
  const checkAge = (value) => {
    const { errors } = validator(value).notEmpty().isNumeric().inRange(18, 100);

    errors.length ? showErrors(errors) : (userData.age = value.toString());
  };

  // номер телефона
  const checkPhone = (value) => {
    const { errors } = validator(value).notEmpty().isEqual(12).isPhone();

    errors.length ? showErrors(errors) : (userData.phone = value);
  };

  // пароль
  const checkPassword = (value) => {
    const { errors } = validator(value).notEmpty().isPassword("pwd3");

    errors.length ? showErrors(errors) : (userData.password = value);
  };

  // адрес электронной почты
  const checkEmail = (value) => {
    const { errors } = validator(value).notEmpty().isEmail("email2");

    errors.length ? showErrors(errors) : (userData.email = value);
  };

  // адрес сайта
  const checkUrl = (value) => {
    const { errors } = validator(value).notEmpty().isUrl("url2");

    errors.length ? showErrors(errors) : (userData.url = value);
  };

  // номер кредитной карты
  const checkCard = (value) => {
    const { errors } = validator(value).notEmpty().isEqual(16).isCard();

    errors.length ? showErrors(errors) : (userData.card = value);
  };

  // дата
  // значение данного поля не нуждается в валидации
  const checkDate = (value) => (userData.date = formatDate(value, "ru"));

  // время
  // значение данного поля также не нуждается в валидации
  const checkTime = (value) => (userData.time = `${formatTime(value)}-${formatTime(Number(value) + 2)}`);

  return {
    checkName,
    checkAge,
    checkPhone,
    checkPassword,
    checkEmail,
    checkUrl,
    checkCard,
    checkDate,
    checkTime,
  };
};

Наконец, в файле «validator.js» содержится самое интересное — логика проверки корректности введенных пользователем данных с помощью регулярных выражений:

// импортируем вспомогательные функции
import { secondHelpers } from "./second-helpers.js";

// получаем функцию для проверки валидности номера кредитной карты
const { luhnAlgorithm } = secondHelpers();

// объект с регулярными выражениями
// не все выражения используются в приложении, некоторые приведены лишь для примера
const regex = {
  // только киррилица без учета регистра
  alpha: /[а-яё]+/i,

  // только цифры
  numeric: /\d+/,

  // только латиница без учета регистра или цифры (не используется)
  alphanumeric: /[a-z0-9]+/i,

  // только +7 и еще 10 цифр
  phone: /\+7\d{10}/,

  // только цифры в формате "DD.MM.YYYY" (не используется)
  date: /\d{2}.\d{2}.\d{4}/,

  // только цифры в формате "10:00-12:00" (не используется)
  time: /\d{2}:\d{2}-\d{2}:\d{2}/,

  // пароль
  // минимум 8 символов, одна буква и одна цифра
  pwd1: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/,

  // минимум 8 символов, одна буква, одна цифра и один специальный символ
  pwd2: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#-_?&])[A-Za-z\d@$!%*#-_?&]{8,}/,

  // 8-12 символов, минимум одна заглавная буква, одна строчная буква, одна цифра и один специальный символ
  // (в соответствующей функции используется данный шаблон)
  pwd3: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*#?&-_])[A-Za-z\d@$!%*#?&-_]{8,12}/,

  // адрес электронной почты
  // простой шаблон
  email1: /\S+@\S+\.\S+/,

  // сложный шаблон (используется данный шаблон)
  email2: /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/,

  // адрес сайта
  // без протокола
  url1: /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/,

  // с протоколом (используется данный шаблон)
  url2: /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/,
};

// фабричная функция, возвращающая определенные в ней функции и объект с ошибками
// каждая внутренняя функция возвращает валидатор с переданным ему значением и массивом с ошибками
// это делает возможным выстраивание цепочки из вызовов функций валидатора
export const validator = (value, errors = []) => {
  // функция определяет, является ли значение пустым
  const notEmpty = () => {
    !value.trim() && errors.push("Поле не может быть пустым.");

    return validator(value, errors);
  };

  // функция проверяет, что значение состоит только из русских букв
  const isAlpha = () => {
    !regex.alpha.test(value) &&
      errors.push("Поле может содержать только русские буквы.");

    return validator(value, errors);
  };

  // функция проверяет, что значение состоит только из цифр
  const isNumeric = () => {
    !regex.numeric.test(value) &&
      errors.push("Поле может содержать только цифры.");

    return validator(value, errors);
  };

  // функция проверяет, что значение имеет определенную длину
  const isEqual = (len) => {
    !(String(value).length === len) &&
      errors.push(`Длина поля должна равняться ${len}.`);

    return validator(value, errors);
  };

  // функция проверяет, что значение находится в заданном диапазоне
  const inRange = (min, max) => {
    (value < min || value > max) &&
      errors.push(
        `Значение поля должно находиться в диапазоне от ${min} до ${max}.`
      );

    return validator(value, errors);
  };

  // функция проверяет, что значения является номером телефона
  const isPhone = () => {
    !regex.phone.test(value) &&
      errors.push("Номер телефона не соответствует шаблону.");

    return validator(value, errors);
  };

  // функция проверяет, что значения является паролем и соответствует установленному шаблону
  const isPassword = (pattern) => {
    !regex[pattern].test(value) &&
      errors.push("Пароль не соответствует шаблону.");

    return validator(value, errors);
  };

  // функция проверяет, что значения является адресом электронной почты
  const isEmail = (pattern) => {
    !regex[pattern].test(value) &&
      errors.push("Адрес электронной почты не соответствует шаблону.");

    return validator(value, errors);
  };

  // функция проверяет, что значения является адресом сайта и соответствует установленному шаблону
  const isUrl = (pattern) => {
    !regex[pattern].test(value) &&
      errors.push("Адрес сайта не соответствует шаблону.");

    return validator(value, errors);
  };

  // функция проверяет, что значения является валидным номером кредитной карты
  const isCard = () => {
    !luhnAlgorithm(value) &&
      errors.push("Значение поля должно быть номером кредитной карты.");

    return validator(value, errors);
  };

  return {
    notEmpty,
    isAlpha,
    isNumeric,
    isEqual,
    inRange,
    isPhone,
    isPassword,
    isEmail,
    isUrl,
    isCard,
    errors,
  };
};

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

Валидация формы средствами серверного JavaScript


Сервер будет реализован на Node.js.

Для проверки корректности введенных пользователем данных потребуется несколько пакетов из реестра npm.

Устанавливаем эти пакеты: yarn add (или npm i) express express-validator cors helmet.

Пакет «express» используется для создания сервера, «cors» — для разрешения запросов из других источников, «helmet» — набор утилит, связанных с безопасностью, «express-validator» — обертка над «validator», используемая для валидации и обезвреживания данных.

express-validator, cors и helmet — это middleware, т.е. промежуточное программное обеспечение или промежуточный слой между HTTP-запросами и ответами.

Также устанавливаем nodemon в качестве зависимости для разработки с целью автоматической перезагрузки сервера при изменении файла «index.js»: yarn add -D nodemon.

Добавляем в файл «package.json» следующие скрипты:

"scripts": {
  "start": "node index.js",
  "dev": "nodemon index.js"
}


Сервер для разработки запускается с помощью команды «yarn (или npm run) dev».

Файл «index.js» выглядит так:

// подключаем пакеты
const express = require("express");
const cors = require("cors");
const helmet = require("helmet");
const { body, validationResult } = require("express-validator");

// создаем экземпляр приложения "express"
const app = express();

// подключаем middleware
// express.json используется для разбора (парсинга)
// поступающих запросов в формате JSON в объект Request
app.use(express.json());
app.use(cors());
app.use(helmet());

// настраиваем валидацию
// body означает, что осуществляется проверка данных, содержащихся в теле запроса (req.body)
// для данных, содержащихся в других свойствах запроса, используется check
// notEmpty - строка не является пустой
// isAlpha - строка состоит только из букв, аргумент - локализация
// escape - обезвреживание данных, например, символ "<" преобразуется в <
// isNumeric - значение состоит только из цифр, аргумент - объект с настройками
// isMobilePhone - значение является номером телефона, аргументы - локализация, strictMode: true означает, что значение должно содержать символ "+" и код в зависимости от локализации
// custom - кастомная валидация
const validationMiddleware = [
  body("name").notEmpty().isAlpha("ru-RU").trim().escape(),
  body("age")
    .isNumeric({
      no_symbols: true,
    })
    .isLength({ min: 2, max: 3 }),
  body("phone").isMobilePhone("ru-RU", {
    strictMode: true,
  }),
  body("password").custom((value) => {
    const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*#?&-_])[A-Za-z\d@$!%*#?&-_]{8,12}/;

    if (!regex.test(value)) throw new Error();

    return true;
  }),
  body("email").isEmail().normalizeEmail(),
  body("url").isURL(),
  body("card").isCreditCard(),
  body("date").custom((value) => {
    const regex = /\d{2}.\d{2}.\d{4}/;

    if (!regex.test(value)) throw new Error();

    return true;
  }),
  body("time").custom((value) => {
    const regex = /\d{2}:\d{2}-\d{2}:\d{2}/;

    if (!regex.test(value)) throw new Error();

    return true;
  }),
];

app.use(express.static(__dirname));

// GET-запрос возвращает тестовую страницу
app.get("/", (_, res) => {
  res.status(200).sendFile("index.html");
});

// обрабатываем POST-запросы, второй параметр - валидатор
app.post("/server", validationMiddleware, (req, res) => {
  // получаем объект с ошибками от валидатора
  // преобразуем его в массив
  // и извлекаем названия полей, не прошедших проверку
  const errors = validationResult(req)
    .array()
    .map((error) => `Некорректное значение поля "${error.param}".`);

  console.log(errors);

  // если массив с ошибками не является пустым
  // отклоняем запрос со статусом 400 (Bad Request)
  // и отправляем ошибки клиенту
  // иначе, сообщаем клиенту, что данные прошли валидацию
  if (errors.length) {
    res.status(400).json(errors);
  } else {
    res.status(201).json("Данные успешно отправлены.");
  }
});

// запускаем сервер
const PORT = process.env.PORT || 1234;
app.listen(PORT, () => console.log(`Сервер запущен. Порт: ${PORT}.`));

Тестовая страница выглядит следующим образом:

<head>
    <title>Form Validation Server</title>
    <style>
        body {
            text-align: center;
        }
        a {
            text-decoration: none;
            font-size: 1.6rem;
        }
    </style>
</head>
<body>
    <h1>Сервер валидации формы</h1>
    <p><a href="https://harryheman.github.io/form-validation-example/">Клиент</a></p>
</body>

Отлично, приложение готово. Теперь займемся разворачиванием сервера на Heroku, а клиента на GitHub Pages.

Деплой приложения


Начнем с сервера.

Заходим на heroku.com, регистрируемся, если не зарегистрированы, нажимаем кнопку «New» в правом верхнем углу, выбираем «Create new app» в выпадающем списке, вводим название приложения, выбираем регион и нажимаем кнопку «Create app». Вкладку браузера оставляем открытой.

Скачиваем и устанавливаем Heroku CLI.

Заходим в нашу директорию «server». Создаем файл ".gitignore", добавляем в него «node_modules». Открываем терминал и вводим «heroku login».

Инициализируем git с помощью «git init». Вводим следующие команды:

// form-validation-example - название приложения
heroku git:remote -a form-validation-example
git add .
git commit -am "create"
git push heroku master

Ждем, пока приложение развернется. Нажимаем кнопку «Open app» в правом верхнем углу. Если видим тестовую страницу, значит, все в порядке.









Переходим к клиенту.

Заходим на github.com, нажимаем кнопку «New» в правом верхнем углу, вводим название репозитория, описание (опционально), нажимаем кнопку «Create repository».

Заходим в нашу директорию «form-validation-example». Создаем в ней файл ".gitignore", добавляем в него «server». Открываем терминал и вводим следующие команды:

git init
git commit -m "create"
git branch -M main
// form-validation-example - название репозитория
git remote add origin https://github.com/harryheman/form-validation.git
git push -u origin main

В репозитории выбираем вкладку «Settings», в разделе «GitHub Pages» в подразделе «Source» нажимаем «None», выбираем «main», нажимаем кнопку «Save». Ждем, пока приложение развернется. Переходим по ссылке.













Нажимаем кнопку «Данные», затем «Отправить». Если видим сообщение «Данные успешно отправлены», значит, все работает.




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

Благодарю за внимание и хорошего дня.