Isomorphic-validation — Javascript библиотека, облегчающая валидацию пользовательского ввода

от автора

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

Я выдвинул следующие требования к библиотеке:

  • Группирование валидаций, чтобы состояние валидности группы зависело от каждой валидации в группе;

  • Возможность «подключения» эффектов пользовательского интерфейса к состояниям валидаторов, валидаций и групп валидаций;

  • Приминение валидаций в качестве обработчиков событий на стороне клиента и в качестве Express middleware на стороне сервера;

  • Возможность разделения клиентских валидаторов и эффектов от серверных;

  • Одинаковое использование синхронных и асинхронных валидаторов;

  • Удобство реализации любой стратегии валидации и на любой тип события формы;

  • Совместимость с библиотеками интернационализации.

Прежде всего я попрошу вас взглянуть на следующий образец кода и, не видя результата его выполнения, попытаться предположить, что он делает:

form.addEventListener(   'input',   Validation.group(      Validation(form.email)       .constraint(isEmail, { err: 'Must be in the E-mail format.' })       .validated(applyOutline(form.email, delayedOutline))       .changed(applyOutline(form.email, changedOutline))       .validated(applyBox(form.email, validIcon))       .validated(applyBox(form.email, errMsg)),      Validation(form.password)       .constraint(isStrongPassword, { err: 'Min. 8 characters, 1 capital letter, 1 number, 1 special character.' })       .validated(applyOutline(form.password, delayedOutline))       .changed(applyOutline(form.password, changedOutline))       .validated(applyBox(form.password, validIcon))       .validated(applyBox(form.password, errMsg)),    )   .changed(applyAccess(form.submitBtn, delayedAccess)) );

Здесь мы группируем два объекта валидации в один и привязываем к изменению его состояния валидности эффект, который в зависимости от этого состояния устанавливает доступ к кнопке отправки формы (аттрибут disabled). К состоянию каждой отдельной валидации в группе тоже привязаны эффекты: отображение иконки, обводки и сообщения о некорректных данных.

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

Песочница с полной версией этого примера находится здесь.

Мотивация

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

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

Концепция

Библиотека не предоставляет готовых валидаторов (эту задачу выполняют другие библиотеки). Здесь логика, используемая в качестве валидаторов, должна быть завернута в функцию-предикат, на входе валидируемое значение, на выходе булево значение или промис с булевым значением. Я обычно использую валидаторы из библиотеки validator.js.

Библиотека экспортирует две сущности Validation и Predicate, а так же набор функций эффектов пользовательского интерфейса и хелперов.

Validation — это сущность с состоянием и логикой. Может быть одиночной или групповой ( а еще «склеенной»). Состояние группы зависит от состояния каждой валидации в группе. А валидатор, добавленный группе, добавляется к каждой отдельной валидации в группе:

Validation.group(    Validation(form.firstName),    Validation(form.lastName),  ) .constraint(isAlpha, { msg: 'Только буквы разрешены.' }) .constraint(isLongerOrEqual(2), { msg: 'Не короче 2 символов.' }) .constraint(isShorterOrEqual(32), { msg: 'Не длиннее 32 символов.' })
Группа валидаций

Группа валидаций

«Склеенная» валидация — это тоже группа, отличается от обычной группы тем, что валидатор получает на вход валидируемые значения всех валидаций в группе, и состояние валидности всех валидаций в группе зависит от исполнения данного валидатора (взаимозависимые поля пароль и подтверждение пароля):

Validation.glue(   Validation(form.password),    Validation(form.pwdConfirm), ) .constraint(equals, { msg: 'Пароль и подтверждение пароля должны совпадать.' })

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

Predicate — это обвертка, необходимая для подключения эффектов к состояниям валидаторов и передачи сообщений до их добавления к валидации:

Validation()   .constraint(     Predicate(isEmailRegistered, { msg: 'Данный адрес уже зарегистрирован.' })       .started(doSomething)       .invalid(doSomethingElse)   )

Каждая валидация и валидатор имеют ряд состояний (конечный автомат), к которым можно подключить эффекты:

Validation.group(      Validation()     .constraint(       Predicate(isSomething)         .started(doSomething)         .valid(doSomething)         .invalid(doSomething)         .changed(doSomething)         .validated(doSomething)     )     .started(doSomething)     .valid(doSomething)     .invalid(doSomething)     .changed(doSomething)     .validated(doSomething) ) .started(doSomething) .valid(doSomething) .invalid(doSomething) .changed(doSomething) .validated(doSomething)

Сгруппированные валидации доступны через свойство .validations а добавленные валидаторы через свойство .constraints:

Validation.group(   Validation(form.firstName),   Validation(form.lastName), ) .validations.forEach( // <--   validation => {     validation.validated(doSomething);   } );  //--------------------------------------  Validation.group(      Validation(form.email)     .constraint(isEmail),      Validation(form.password)     .constraint(isStrongPassword), ) .constraints.forEach( // <--   (validator, formField) => {     validator.validated(applyOutline(formField));   } );

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

const emailV = Validation()   .constraint(isMinLength(8), { next: false })   .constraint(isMaxLength(48), { next: false })   .constraint(isEmail, { next: false });  const passwordV = Validation()   .constraint(isStrongPassword);  const pwdConfirm = Validation();  Validation.glue(passwordV, pwdConfirm)   .constraint(equals);  const [signupForm, signupV] = Validation.profile(   '[name="signupForm"]',   ['email', 'password', 'pwdConfirm'],   [emailV, passwordV, pwdConfirm], );

Теперь валидация signupV готова к использованию, как на клиенте, так и на сервере:

// на клиенте signupForm.addEventListener('input', signupV);  // на сервере app.post('/signup', urlencodeParser, signupV, signupHandler);

А это если необходимо отделить клиентские валидаторы и эффекты от серверных:

Validation()   .client // <--   .validated(doSomethingOnClient)  //-----------------------------------  Validation()   .server // <--   .invalid(doSomethingOnServer)

Например, таким образом можно отделить валидатор на клиенте, делающий запрос к серверу, и валидатор на сервере делающий запрос к базе данных:

const emailV = Validation()   .constraint(isEmail, { next: false }) // выполнится на клиенте и на сервере   .client   .constraint(isEmailRegisteredC); // выполнится только на клиенте  //-----------------------------------  emailV   .server   .constraint(isEmailRegisteredS); // выполнится только на сервере

Валидаторы можно выполнять условно и с задержкой. Задержка может быть полезна для валидаторов, делающих запросы, при валидации по событию input. Для этого вместе с валидатором нужно передать объект с опциями:

const emailV = Validation()   .constraint(isEmail, { next: false }) // следующий не будет выполнен, пока текущий не вернет true   .constraint(isEmailRegisteredC, { debounce: 5000 }); // запрос на сервер

Дополнительно в этот объект с опциями можно поместить любые данные в любом виде, например сообщения или ключи переводов. Они будут проброшены под теми же именами в результат валиции. Пример использования совместно с библиотекой i18next можно найти здесь.

Эффекты пользовательского интерфейса

Отдельно от основного модуля библиотека предоставляет набор типичных для задач валидации эффектов, таких как: управление доступом к DOM элементам, изменение фона, отображение иконок и сообщений, добавление класса, добавление обводки.

Можно задавать разные значения эффектов и разную задержку их приминения в зависимости от валидности:

const delayedOutline = {   // при невалидном состоянии, розовая обводка, спустя 2 секунды   false: { delay: 2000, value: '2px solid lightpink' },   // при валидном состоянии, убрать обводку, спустя пол секунды   true: { delay: 500, value: '' }, };  Validation(form.email)   .constraint(isEmail)   .validated(applyOutline(delayedOutline))   .validate();

Но что делать если состояние, к которому привязан отложенный эффект, изменилось раньше, чем он наступил и для нового состояния он не актуален? Для этого используется идентификатор эффекта. Эффекты с одним и тем же идентификатором, примененные к одному и тому же элементу, отменяют друг друга:

const delayedOutline = {   false: { delay: 2000, value: '2px solid lightpink' },   true: { delay: 500, value: '2px solid green' }, };  const clearOutline = {   false: { delay: 500, value: '' },   true: { delay: 500, value: '' }, };  const outlineEID = 'outline'; // <--  Validation(form.email)   .constraint(isEmail)   .started(applyOutline(clearOutline, outlineEID)) // <--   .validated(applyOutline(delayedOutline, outlineEID)) // <--   .validate();

А вообще эффект функция возвращает пару функций, одна для отмены отложенного эффекта, другая для его установки:

const [cancel, set] = applyOutline();

Возможно этот паттерн кому-то что-то напоминает, особенно если заменить «apply» на «use».

Также вместо приминения библиотечных эффектов возможно привязать состояния компонентов к состояниям валидаций и валидаторов.

Приминение на клиенте и сервере

Приминение одних и тех же валидаций на клиенте и сервере сводится к нескольким шагам:

  1. Подготовка валидаторов и сообщений.

  2. Создание валидаций.

  3. Добавление эффектов пользовательского интерфейса.

  4. Создание профилей.

Граф зависимостей в проекте с формой регистрации и формой входа может выглядеть примерно так:

Приминение одних и тех же валидаций на клиенте и сервере.

Приминение одних и тех же валидаций на клиенте и сервере.

Здесь index.js — это сервер, а signup.js и signin.js — точки входа для сборщика модулей. Стоит отметить, что обе формы используют одни и те же валидации эл.почты и пароля. Однако на форме регистрации добавляются проверки на равенство пароля и его подтверждения а так же проверка регистрации адреса эл.почты, при которой делается запрос на сервер. В свою очередь на сервере добавляется проверка на существование адреса эл.почты в базе данных.

Пошаговая инструкция с данным примером находится здесь.

Что мы имеем в результате:

  • Нет дублирования кода валидации между разными формами с полями одного типа.

  • Нет дублирования кода валидации между клиентом и сервером.

  • Вся логика валидации находится в одном месте, что обеспечивает один источник истины.

И на последок попрошу вас ответить на один вопрос.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Как вы считаете, имеет ли такая библиотека право на существование?

66.67% Да, имеет.2
0% Врядли.0
33.33% Трудно сказать.1
0% Я не понял(а), какую проблему решает библиотека и зачем она вообще нужна.0

Проголосовали 3 пользователя. Воздержались 2 пользователя.

ссылка на оригинал статьи https://habr.com/ru/articles/907724/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *