Я начал этот небольшой проект под названием 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».
Также вместо приминения библиотечных эффектов возможно привязать состояния компонентов к состояниям валидаций и валидаторов.
Приминение на клиенте и сервере
Приминение одних и тех же валидаций на клиенте и сервере сводится к нескольким шагам:
-
Подготовка валидаторов и сообщений.
-
Создание валидаций.
-
Добавление эффектов пользовательского интерфейса.
-
Создание профилей.
Граф зависимостей в проекте с формой регистрации и формой входа может выглядеть примерно так:
Здесь index.js — это сервер, а signup.js и signin.js — точки входа для сборщика модулей. Стоит отметить, что обе формы используют одни и те же валидации эл.почты и пароля. Однако на форме регистрации добавляются проверки на равенство пароля и его подтверждения а так же проверка регистрации адреса эл.почты, при которой делается запрос на сервер. В свою очередь на сервере добавляется проверка на существование адреса эл.почты в базе данных.
Пошаговая инструкция с данным примером находится здесь.
Что мы имеем в результате:
-
Нет дублирования кода валидации между разными формами с полями одного типа.
-
Нет дублирования кода валидации между клиентом и сервером.
-
Вся логика валидации находится в одном месте, что обеспечивает один источник истины.
И на последок попрошу вас ответить на один вопрос.
ссылка на оригинал статьи https://habr.com/ru/articles/907724/
Добавить комментарий