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

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

Formik

Сначала создаем форму регистрации с библиотекой Formik. Мне нравится Formik, потому что он легкий, почти не имеет библиотеки абстракций, которая поможет вам создавать формы в React. Если вам не нужен Redux (You Might Not Need Redux - Dan Abramov - Medium, Стоит ли хранить состояние формы в Redux? - Гоша Аринич), это очень хороший выбор. В нашем примере Formik помогает нам сохранять состояние (значения, ошибки и то, отправляется ли форма) и обрабатывать изменения. Он также вызывает функцию проверки для нас при каждой отправке. Я использую Formik как компонент React с render prop, но вы также можете использовать его как HoC.

export default function SignUpFormContainer() {
  return (
    <Formik
      initialValues={initialValues}
      validate={validate}
      onSubmit={onSubmit}
      render={SignUpForm}
    />
  )
}
function SignUpForm(props) {
  const {
    isSubmitting,
    errors,
    handleChange,
    handleSubmit,
  } = props
return (
    <div className="form">
      <label className="form-field" htmlFor="email">
        <span>E-mail:</span>
        <input
          name="email"
          type="email"
          onChange={handleChange}
        />
      </label>
      <div className="form-field-error">{errors.email}</div>
      <label className="form-field" htmlFor="password">
        <span>Password:</span>
        <input
          name="password"
          type="password"
          onChange={handleChange}
        />
      </label>
      <div className="form-field-error">{errors.password}</div>
      <label
        className="form-field"
        htmlFor="passwordConfirmation"
      >
        <span>Confirm password:</span>
        <input
          name="passwordConfirmation"
          type="password"
          onChange={handleChange}
        />
      </label>
      <div className="form-field-error">
        {errors.passwordConfirmation}
      </div>
      <label className="form-field" htmlFor="consent">
        <span>Consent:</span>
        <input
          name="consent"
          type="checkbox"
          onChange={handleChange}
        />
      </label>
      <div className="form-field-error">{errors.consent}</div>
      <button onClick={handleSubmit}>
        {isSubmitting ? 'Loading' : 'Sign Up'}
      </button>
    </div>
  )
}

Проверка

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

Требования

  • Я не хочу создавать специальный компонент поля формы и добавлять проверку в каждое поле формы отдельно, например <FormField validate={value => someValidation(value)} />. Я считаю, что проверка должна быть частью бизнес-логики, а не пользовательского интерфейса, и я не хочу смешивать ее, если в этом нет необходимости. В противном случае я хотел бы иметь одну функцию проверки для всей формы, которая принимает объект значений формы и возвращает объект с сообщением об ошибке для каждого значения. Этот подход к проверке можно использовать даже для любого другого объекта JS, а не только для проверки формы.
  • Нет if заявлений.
  • Может быть возможность заменить if исключениями. Мне это тоже не нравится. Исключения составляют непредвиденное поведение, но независимо от того, заполнено ли поле формы или нет, это не является неожиданным. Напротив, этого вполне ожидаемо. И, как я уже упоминал, проверка является частью наших бизнес-правил.
  • Одно или несколько правил для каждого поля. Я хотел бы проверить каждый атрибут на соответствие одному или нескольким правилам.
  • Я также хотел бы иметь возможность возвращать больше ошибок для данного поля.
  • Вернуть сообщение об ошибке, различное для каждого правила.
  • Перекрестная проверка между атрибутами, что означает проверку поля в зависимости от другого значения поля.

Я не упомянул ifs, потому что может возникнуть соблазн начать с чего-то вроде этого:

if (!values.email) {
  errors.email = 'E-mail is required!'
}
if (!values.password) {
  errors.password = 'Password is required!'
} else if (values.password.length < 6) {
  errors.password = 'Password has to be longer than 6 characters'
}

Это могло быть очень нечитаемым с большим количеством полей или более сложными правилами, поэтому я подумал: «Можем ли мы сделать это лучше? Должен быть какой-то другой, более декларативный способ, без ifs ».

Ага

К счастью, сам Formik по умолчанию позволяет использовать валидационную библиотеку Yup. Самым простым способом вы можете написать просто validationSchema и передать его как опору <Formik /> компоненту.

const validationSchema = Yup.object().shape({
  email: Yup.string()
    .email('E-mail is not valid!')
    .required('E-mail is required!'),
  password: Yup.string()
    .min(6, 'Password has to be longer than 6 characters!')  
    .required('Password is required!')
})
<Formik 
  ...
  validationSchema={validationSchema} 
  ...
/>

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

В этом случае вы все равно можете использовать Yup, но вместе со своей собственной функцией проверки. Создаем getValidationSchema функцию. Он принимает объект values и возвращает схему проверки, аналогичную приведенной выше в примере.

function getValidationSchema(values) {
  return Yup.object().shape({
    email: Yup.string()
      .email('E-mail is not valid!')
      .required('E-mail is required!'),
    password: Yup.string()
      .min(6, 'Password has to be longer than 6 characters!')
      .required('Password is required!'),
    passwordConfirmation: Yup.string()
      .oneOf([values.password], 'Passwords are not the same!')
      .required('Password confirmation is required!'),
    consent: Yup.bool()
      .test(
        'consent',
        'You have to agree with our Terms and Conditions!',
        value => value === true
      )
      .required(
        'You have to agree with our Terms and Conditions!'
      ),
  })
}

Это довольно просто. Я просто хочу выделить здесь два момента:

  1. passwordConfirmation проверка: насколько я знаю, у Yup нет ничего похожего на equals метод для сравнения двух строк. Нам нужно сравнить значение passwordConfirmation со значениями массива oneOf(array, 'Error message'), где array - это массив только с одним значением из values объекта values.password.
  2. consent проверка: да, также нет метода, чтобы проверить, является ли значение true или false. Нам нужно использовать метод test, который принимает 3 параметра. Первое - это название проверки (не знаю почему 🤔), второе - сообщение об ошибке, а третье - наша настраиваемая функция проверки, которая принимает value в качестве параметра и должна возвращать true или false. Мы используем просто простой value => value === true.

Затем мы просто вызываем метод validateSync() для этой схемы:

function validate(values) {
  validate = (values) => {
    const validationSchema = getValidationSchema(values)
    try {
      validationSchema.validateSync(values, { abortEarly: false })
      return {}
    } catch (error) {
      return getErrorsFromValidationError(error)
    }
  }
}

Это небольшая проблема, потому что validateSync метод не возвращает объект с ошибкой, а выдает ValidationError. Было бы лучше, если бы он возвращал объект ошибок напрямую. В любом случае, я написал еще одну простую функцию, которая принимает этот объект ошибки и возвращает объект ошибок в той форме, которая нам нужна (только с первым сообщением об ошибке проверки для каждого поля формы):

function getErrorsFromValidationError(validationError) {
  const FIRST_ERROR = 0
  return validationError.inner.reduce((errors, error) => {
    return {
      ...errors,
      [error.path]: error.errors[FIRST_ERROR],
    }
  }, {})
}

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

Плюсы:

  • Декларативная
  • Предопределенные методы проверки (email, min, max, required)

Минусы:

  • Вы должны изучить его API
  • Проверка выполняется по исключению

Ожидаемый

В Formik замечательно то, что вы можете использовать любую библиотеку валидации, какую захотите. Вам просто нужно использовать функцию validate вместо специфической для Yup validationSchema. Я понимаю, что было бы хорошо использовать библиотеку проверки Spected также для проверки формы, потому что мы уже используем ее для проверки объектов JS на бэкэнде в нашем проекте на работе.

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

Начнем снова с функции getValidationSchema:

function getSpectedValidationSchema(values) {
  return {
    email: [
      [value => !isEmpty(value), 'E-mail is required!'],
      [value => isEmail(value), 'E-mail is not valid!'],
    ],
    password: [
      [value => !isEmpty(value), 'Password is required!'],
      [
        value => value.length >= 6,
        `Password has to be longer than 6 characters!`,
      ],
    ],
    passwordConfirmation: [
      [
        value => !isEmpty(value),
        'Password confirmation is required!',
      ],
      [
        value => value === values.password,
        'Passwords are not the same!',
      ],
    ],
    consent: [
      [
        value => value === true,
        'You have to agree with our Terms and Conditions!',
      ],
    ],
  }
}

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

Наша функция проверки выглядит немного иначе. Теперь мы не обрабатываем исключение, но нам все равно нужно обработать результат проверки, чтобы соответствовать нашему требованию (объект ошибок с полем формы в качестве ключа и сообщением об ошибке в качестве значения):

function validate(getValidationSchema) {
  return (values) => {
    const spec = getValidationSchema(values)
    const validationResult = spected(spec, values)
    return getErrorsFromValidationResult(validationResult)
  }
}
function getErrorsFromValidationResult(validationResult) {
  const FIRST_ERROR = 0
  return Object.keys(validationResult).reduce((errors, field) => {
    return validationResult[field] !== true
      ? { ...errors, [field]: validationResult[field][FIRST_ERROR] }
      : errors
  }, {})
}

Есть одна проблема с Spected - определение обязательных полей формы. Например, если объект values не содержит consent, функция проверки для consent не вызывается. Это потому, что Spected не перебирает неопределенные атрибуты.

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

const requiredFields = {
  email: undefined,
  password: undefined,
  passwordConfirmation: undefined,
  consent: undefined,
}
const valuesWithRequiredFields = { ...requiredFields, ...values }

Плюсы:

  • Декларативный, но более функциональный подход.
  • Простой легкий API.
  • Проверка не обрабатывается исключениями.

Минусы:

  • Отсутствует проверка обязательных полей, не содержащихся в проверенном объекте.
  • Нет предопределенных функций проверки.

Вы можете найти полный пример кода здесь: GitHub - jakubkoci / react-form-validation: Simple React form validation with Formik and Yup.