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

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

Здесь я расскажу о нескольких типичных случаях проверки, немного более сложных, чем в формах ToDo / Sign Up / Login ...

Я использую слово «форма», но можно легко рассмотреть всю страницу. В одном из наших вариантов использования у нас есть «форма», которая разделена на три панели (левое меню, холст, инспектор свойств), и мы выполняем проверку на всех этих панелях. Это один из случаев, когда идея «формы» выходит из-под контроля.

Пример приложения

Не слишком сложно, но все же не так просто… Вы можете увидеть это бег на героку.

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

Посмотрите, как возникла валидация, проследив за историей коммитов. Это ни в коем случае не содержит ошибок, и есть место для улучшений по всем направлениям. На самом деле основное внимание уделяется части проверки - остальную часть приложения я собрал быстро и более простыми способами.

Требования к валидации

  • Имя приложения должно быть заполнено
  • Должна быть хотя бы одна допустимая конфигурация
  • Если есть дополнительные конфигурации, они могут быть частичными.
  • Состояние каждой конфигурации отображается рядом с ее названием в левом меню.
  • Пользовательское поле владельца проверяется асинхронно, показывая статус загрузки
  • Предложения должны быть показаны пользователю (оранжевый текст)
  • Форма не может быть отправлена, если она не действительна или проверка не завершена.

Начальное состояние

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

Добавляется первая конфигурация

После того, как пользователь нажмет кнопку «New Config», появятся дополнительные поля. Он вводит «Разработка» в качестве имени конфигурации. Необходимо заполнить еще два поля - файл дескриптора необязательных свойств и пользователь-владелец. Пользователь-владелец может быть пустым или должен иметь действующий адрес электронной почты, подтвержденный вызовом API. Пользователь может видеть общий статус конфигурации в меню конфигураций и подсказку рядом с кнопкой отправки. Отправить по-прежнему отключено.

Действительная форма, готовая к отправке

Конфигурация разработки все настроена, как и производственная конфигурация. Есть еще две конфиги с некоторыми проблемами - Stage и Test. Несмотря на то, что у нас есть по крайней мере одна действующая конфигурация, и теперь включена отправка.

Структура приложения и повторный выбор на сцене

Есть два контейнера - AppContainer и Configs. В демонстрационных целях есть также компонент Config.

AppContainer включает поле ввода имени, основную область и нижний колонтитул с кнопкой отправки. Он подключен к хранилищу redux и использует некоторые результаты проверки с помощью селекторов декораций (подробнее об этом на github).

Контейнер конфигураций обрабатывает рендеринг и оформление левого меню, отображение выбранной в данный момент конфигурации и добавление новой конфигурации. Также он создает реквизиты, необходимые для компонента Config (значения полей, действия, оформление).

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

Дизайн высокого уровня

В вашем контейнере или компоненте вы обычно хотите сделать какое-то выделение или показать сообщения проверки. Это доступно с помощью селекторов и mapStateToProps

class SomeForm extends Component {
  render() {
    const decor = this.props.validationDecoration;
    const message = decor.nameMessage || null;
    return (
      <div>
        <input onChange={ ... } className={ decor.nameClasses }/>
        { message }
      </div>
    );
  }
}

const mapStateToProps = state => ({
  name: nameSelector(state),
  validationDecoration: formDecorationSelector(state)
});

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

const nameSelector = state => state.form.get('name');
const isNameValidSelector = createSelector(
  [nameSelector],
  (name) => {
    return name && name.trim() !== '';
  }
);
export const formDecorationSelector = createSelector(
  [isNameValidSelector],
  (isNameValid) => {
    return {
      nameClasses: isNameValid ? '' : 'error',
      message: isNameValid ? '' : 'Name has to be provided'
    };
  }
);

Если вам нужны более конкретные сообщения, вы можете вернуть их из селекторов проверки или использовать константы, I18n, кодовые имена ...

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

export function validation(state = initialState, action) {
  switch (action.type) {
    case types.NAME_VERIFICATION_REQUEST:
      return state.validation.
        set('nameVerified', false).
        set('nameVerificationRunning', true);
    case types.NAME_VERIFICATION_SUCCESS:
      return state.validation.
        set('nameVerified', true).
        set('nameVerificationRunning', false);

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

const isNameVerifiedSelector =
  state => state.validation.get('nameVerified');

Что не сработало

Перед повторным выбором мы пробовали разные подходы, но в наших вариантах использования они не сработали.

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

Как только мы начали играть с react и redux, мы хотели избежать этих проблем. Самым первым, что у нас было, была проверка сверху вниз, когда вся форма проверялась с помощью редуктора. По сути, из каждого оператора case вызывалась функция, которая должна была что-то делать с проверенными полями. Он предоставит все, что в конечном итоге понадобится форме, а затем компоненты и контейнеры будут передавать результаты проверки по цепочке свойств. Вы можете себе представить, как выглядела передача реквизита, или какие проблемы, если вы хотели разделить редюсер на несколько более мелких. Проверка должна быть разделена или вызываться между редукторами. Мы не хотели, чтобы нас заставляли формировать редукторы для выполнения требований валидации.

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