Руководство по созданию собственных сложных проверенных форм реакции

Прочтите оригинальную статью в блоге Sicara здесь.

Библиотеки хороши, но часто не подходят для решения ваших задач. В этой статье представлена ​​безбиблиотечная среда для написания сложных вложенных проверенных форм в React.

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

По своему опыту я хотел создать:

  • динамические формы: пользователь может добавлять поля (например, в список дел), и содержание одних полей ввода может изменять содержание и / или отображение других.
  • проверенные формы: форма должна иметь внешнюю декларативную проверку, запускаемую по запросу.
  • вложенные формы: некоторые формы являются полями основной формы или подчиненных форм.
  • пользовательские компоненты ввода: либо использовать базовые входы HTML 5, либо, например, материальные элементы пользовательского интерфейса.

Мое приложение для реагирования использовало redux store. Это очень часто, но часто вызывает путаницу. Как отмечает React team:

С самого начала мы должны подчеркнуть, что redux не имеет отношения к реакции

Одна из наиболее часто используемых библиотек для вложенных форм в React - это redux-forms.

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

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

Рекомендации по React

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

  • данные формы должны храниться где-то в состоянии компонента (или в хранилище redux). Есть один объект для всей формы (единый источник истины), т.е. что на вход не одна переменная, а входы являются ключами одного и того же dataobject.
  • вы можете иметь контролируемые или неуправляемые компоненты реакции:
  • контролируемый означает, что данные, отображаемые пользователю, являются данными, которые вы передаете компоненту. В этой настройке вы должны обрабатывать каждый ввод, чтобы вы обновляли данные, и у пользователя все еще было ощущение, что он фактически заполняет ввод.
  • неконтролируемый означает, что отображаемые данные - это данные, введенные пользователем. Вы по-прежнему можете отображать значение по умолчанию при монтировании, но нет способа принудительно отображать значение, когда пользователь начинает вводить

Если вы решите использовать управляемый компонент, вам действительно не нужно помещать свои входные данные в тег <form>, потому что вы уже получаете данные в любое время. С неконтролируемым компонентом вы можете либо обрабатывать изменения данных, либо использовать тег <form> для обработки данных при отправке.

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

Вложенные формы

Вложенные формы - это формы, в которых данные представляют собой не только примитивные типы, но также массивы или объекты. По сути, я думаю, что вложенные формы не меняют того, что было сделано до сих пор. Но вы должны быть осторожны и осторожно применять фреймворк, чтобы избежать неожиданных изменений в данных формы.

Основная форма

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

const data = {
  key1: 'key1',
  key2: {...},
  key3: [{...}, {...}],
}

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

На верхнем уровне key1, key2 и key3 - это блоки данных данных вашей формы. Таким образом, вы должны иметь в основном рендере три компонента, по одному для каждого из этих ключей. Например:

render() {
  return (
    <input
      type='text'
      name='key1'
      value={this.state.data.key1}
    />
    <SubFormComponentForKey2DataType
      value={this.state.data.key2}
    />
    <ArrayOfSubFormComponentsForKey3DataType
      value={this.state.data.key3}
    />
  );
}

В последнем случае может возникнуть соблазн использовать напрямую map:

render() {
  return (
    <input
      type='text'
      name='key1'
      value={this.state.data.key1}
    />
    <SubFormComponentForKey2DataType
      value={this.state.key2}
    />
    {this.state.data.key3.map((subform) =>
      <SubFormComponentForKey3DataType
        key={subform.key}
        value={subform}
      />
    }
  )
}

Эта практика не приветствуется тем фактом, что нужно обрабатывать изменения каждого SubFormComponentForKey3DataTypecomponent в соответствии с их индексом. Тем самым мы нарушаем сходство с другими полями (например, key1 и key2). В случае тяжелых форм нужно действительно думать о мелочах, разделяй и властвуй. Другими словами, не требуйте от компонента слишком многого, делайте его простым и похожим, разделяйте логику.

В основном компоненте определяется метод обработки таких изменений:

handleFieldChange = (field) => (event, value, selectedKey) => {
  // make a copy of the object first to avoid changes by reference
  let data = { ...this.state.data };
  // use here event or value of selectedKey depending on your component's event
  data[field] = value;
  this.setState({ data });
}

«Сохраняйте простоту» означает, что для очень простой формы метод должен только уметь обрабатывать key1, key2 или key3 в целом. Вы никоим образом не хотите сообщать ему об изменении key2.subkey1 или изменении подполя данных key3. Затем этот метод нужно передать вашим компонентам в рендере:

render() {
  return (
    <input
      type='text'
      name='key1'
      value={this.state.data.key1}
      onChange={this.handleFieldChange('key1')}
    />
    <SubFormComponentForKey2DataType
      value={this.state.data.key2}
      onChange={this.handleFieldChange('key2')}
    />
    <ArrayOfSubFormComponentsForKey3DataType
      value={this.state.data.key3}
      onChange={this.handleFieldChange('key3')}
    />
  )
}

Подчинение как объект

Теперь посмотрим, что должно быть внутри <SubFormComponentForKey2DataType />. Помните, что для всей формы есть только один state. Это означает, что подчиненные формы получают свою часть в props и вызывают метод своего родителя при изменении. Таким образом, handleFieldChange:

handleFieldChamge = (field) => (event, value, selectedKey) => {
  let data = { ...this.props.value };
  data[field] = value;
  // you could pass the event here but also null if it is not necessary nor useful
  this.props.onChange(null, data);
}

Здесь единственная разница между предыдущим методом и этим заключается в том, что он вызывает this.props.onChange вместо того, чтобы напрямую устанавливать state. Еще раз советую сделать это как можно проще.

Массив вложенных форм

Давайте, наконец, рассмотрим случай с map, как в <ArrayOfSubFormComponentsForKey3DataType />. Этот компонент должен иметь возможность обрабатывать изменения только на первом уровне, т.е. в массиве объектов. Точнее, у него должен быть метод:

handleFieldChange = (index) => (event, value, selectedKey) => {
  let data = [...this.props.value];
  data[index] = value;
  this.props.onChange(null, data);
}

Это очень (очень) похоже на предыдущий метод. На самом деле единственная разница в том, что он разрушает массив, а не объект.

Наконец, рендер должен содержать только:

render() {
  return (
    {this.props.value.map((subform, index) =>
      <SubFormComponentforKey3DataType
        key={subform.key}
        value={subform}
        onChange={this.handleFieldChange(index)}
      />
    }
  )
}

Таким образом, изменения распространяются от компонента к компоненту, и вам никогда не придется думать о том, что происходит за пределами вашего компонента, или о том, где вы находитесь в данных формы.

Этот параметр очень прост: вы обрабатываете в компоненте только изменения его значений первого уровня. Компонент может обрабатывать изменение, а затем вызывает родительский метод, означающий «моя работа сделана, делайте с ней то, что вы хотите».

Проверка данных

Валидатор форм

HTML5 предоставляет простые средства проверки данных прямо в базовых <input> компонентах. Например, указание type="number" или required напрямую запретит пользователю вводить что-либо, кроме чисел, или отправлять пустое поле. Прочтите документацию, чтобы ознакомиться с исчерпывающим списком возможностей встроенной проверки формы HTML5. Однако я использовал библиотеку material-ui, поэтому эти валидаторы не были доступны.

Более того, в некоторых случаях вам нужна не только проверка ввода, но и проверка данных, например field1 + field2 < threshold.

По этим причинам я решил сделать ставку на пакет validate.js. Он предоставляет исчерпывающий список валидаторов и простое декларативное определение желаемых ограничений для объекта. Можно определить несколько ограничений для одного поля с разными сообщениями об ошибках. Поскольку мы не хотим думать о проверке данных вне формы, мы определяем formValidator вместе с его ограничениями в том же .js файле:

import { Component } from 'react'
import validate from 'validate.js';
const constraints = {
  key1: {
    presence: { // prebuilt validate.js validators.
      allowEmpty: false,
      message: 'some custom or intl error message',
    },
    numericality: true,
  },
};
export const subformValidator = (data) => validate(data, constraints);
export const Subform extends Component {
  ...
}

Если, с другой стороны, форма имеет вложенные формы, такие как в первом примере, form1Validator должен вызвать subformValidator:

import { subformValidator } from 'components/forms/subform'
export const form1Validator = (data) => {
  let validation = validate(data, constraints);
    if (data.subform) {
    validation = {
      ...validation,
      subform: subformValidator(subform),
    };
  }
  return validation;
}

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

Ошибки отображения

validate.js функция возвращает объект с теми же ключами, что и аргумент. Он содержит либо undefined, если ошибок не было, либо объект с сообщениями об ошибках, если это необходимо.

Затем можно передать объект validation каждому из входных компонентов, чтобы они могли отображать сообщение об ошибке, если это необходимо. Если ошибки нет, принимается undefined и ничего не отображается.

Проверка при отправке

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

Предыдущее рассмотрение приводит к двум моментам внимания:

  • проверка формы, вычисленная в основном компоненте, должна распространяться на дочерние компоненты.
  • объект validation сложнее, чем просто определяемый или undefined, поскольку ключи могут быть определены, но содержать только неопределенные значения.

Первая точка означает, что нужно передавать props каждую проверку вместе с исходным значением:

render() {
  return (
    <input
      type='text'
      name='key1'
      value={this.state.data.key1}
      validation={this.state.validation.key1}
      onChange={this.handleFieldChange('key1')}
    />
    <SubFormComponentForKey2DataType
      value={this.state.data.key2}
      validation={this.state.validation.key2}
      onChange={this.handleFieldChange('key2')}
    />
    <ArrayOfSubFormComponentsForKey3DataType
      value={this.state.data.key3}
      validation={this.state.validation.key3}
      onChange={this.handleFieldChange('key3')}
    />
  )
}

Второй момент был решен с помощью функции util. Он выполняет глубокий поиск undefined, итеративно просматривая объект, чтобы убедиться, что, если ключи определены, они приводят только к undefined значениям.

export const deepUndefinedSearch = (obj) => (
    obj instanceof Object && obj.constructor === Object ?             
    Object.keys(obj)
      .map(
        (key) => obj[key] instanceof Array ?
                 obj[key].map((o) => deepUndefinedSearch(o)).filter((o) => !o).length === 0 :
                 obj[key] === undefined
      )
      .filter((o) => !o)
      .length === 0 :
    obj === undefined
  );

Затем вывод валидатора основной формы может быть обработан этой функцией для возврата либо true, если все формы проверены, либо false в противном случае:

const formIsValidated = (validation) =>         
   deepUndefinedSearch(validation)

Ограничения текущей реализации

Проверка

Пока объекты имеют самопроверку, текущая реализация хоть и многословна, но работает. Однако мы столкнулись с некоторыми случаями, когда объекты для заполнения были определены специально в зависимости от некоторой бизнес-логики, реализованной где-то еще в компоненте. Эта логика не была доступна из самого объекта.

В этом контексте это означает, что каждый экземпляр одного и того же компонента может вести к разным валидаторам. Более вероятно, что рендеринг включает бизнес-логику, которую никто не хочет копировать. В некотором смысле необходимо проверить данные, которые может заполнить пользователь. Затем каждый компонент должен сам проверить свои данные и отправить результат обратно родительскому компоненту.

Атрибут ref можно использовать для определения того, что отображается в рендере. Итак, первая возможность - отфильтровать объект constraints с отображаемыми входными данными. Затем результаты должны быть увеличены через вложенные формы до основного компонента реакции.

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

Можно было бы передать логическое значение каждому дочернему компоненту: displayErrorMessage. Эта переменная может быть инициализирована в false и установлена ​​в true при отправке. По-прежнему валидация вычисляется при изменении.

Факторизация

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

Заключение

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

Я надеюсь, что это поможет другим сэкономить время и получить опыт реагирования! Поделитесь своими мыслями и советами здесь, в твиттере или свяжитесь с нами. Также не стесняйтесь взглянуть на другие наши сообщения в блоге о Node.Js и отреагировать.

Я также выражаю особую благодарность Никласу Ансману Гертцу за прекрасную библиотеку validate.js.