Валидация форм React, часть 2 [Видео]. Работа с файлами в Formik & Yup

В данном видео вы узнаете как работать с файлами в Formik и как их валидировать в yup. Так же вы познакомитесь со способами изменения схемы валидации поля, в зависимости от данных других полей.

Код из видео

import React from 'react';
import { Formik, FieldArray } from 'formik'
import * as yup from 'yup'
import './App.scss'


function App() {

  const getError = (touched, error) => {
    return touched && error && <p key={error} className={'error'}>{error}</p>
  }

  const validationsSchema = yup.object().shape({
    addressRegister: yup.string().required(),
    likeRegister: yup.bool(),
    addressActual: yup.string().when('likeRegister', {
      is: false,
      then: yup.string().required()
    }),
    file: yup.array().of(yup.object().shape({
      file: yup.mixed().test('fileSize', 'Размер файла больше 10 байт', (value) => {
        if (!value) return false
        return value.size < 10
      }).required(),
      type: yup.string().oneOf([`application/vnd.ms-publisher`], 'Добавьте файл с правильным форматов').required(),
      name: yup.string().required()
    }).typeError('Добавьте файл')).required()
  })

  const getFileSchema = (file) => (file && {
    file: file,
    type: file.type,
    name: file.name
  })

  const getArrErrorsMessages = (errors) => {
    const result = []
    errors && Array.isArray(errors) && errors.forEach((value) => {
      if (typeof value === 'string') {
        result.push(value)
      } else {
        Object.values(value).forEach((error) => { result.push(error) })
      }
    })
    return result
  }


  return (
    <div>
      <Formik
        initialValues={{
          addressRegister: '',
          likeRegister: false,
          addressActual: '',
          file: undefined
        }}
        validateOnBlur
        onSubmit={(values) => { console.log(values) }}
        validationSchema={validationsSchema}
      >
        {({ values, errors, touched, handleChange, handleBlur, isValid, handleSubmit, dirty }) => (
          <div className={`from`}>
            <p>
              <label htmlFor={`addressRegister`}>Адрес регистрации</label><br />
              <input
                className={'input'}
                type={`text`}
                name={`addressRegister`}
                onChange={handleChange}
                onBlur={handleBlur}
                value={values.addressRegister}
              />
            </p>
            {getError(touched.addressRegister, errors.addressRegister)}
            <p>
              <label htmlFor={`likeRegister`}>Адреса совпадают
                <input
                  type={`checkbox`}
                  name={`likeRegister`}
                  onChange={handleChange}
                  onBlur={handleBlur}
                  checked={values.likeRegister}
                />
              </label>
            </p>
            {!values.likeRegister &&
              <p>
                <label htmlFor={`addressActual`}>Адрес проживания</label><br />
                <input
                  className={'input'}
                  type={`text`}
                  name={`addressActual`}
                  onChange={handleChange}
                  onBlur={handleBlur}
                  value={values.addressActual}
                />
              </p>
            }
            {!values.likeRegister && getError(touched.addressActual, errors.addressActual)}
            {console.log('file', values.file)}
            {console.log('fileErrors', errors.file)}
            <FieldArray name={`file`}>
              {(arrayHelper) => (
                <>
                  <p>
                    <input
                      type={`file`}
                      name={`file`}
                      onChange={(event) => {
                        const { files } = event.target
                        const file = getFileSchema(files.item(0))
                        if (!file) {
                          arrayHelper.remove(0)
                        }
                        if (Array.isArray(values.file)) {
                          arrayHelper.replace(0, file)
                        } else {
                          arrayHelper.push(file)
                        }
                      }}
                    />
                  </p>
                  {getArrErrorsMessages(errors.file).map((error) => getError(true, error))}
                </>
              )}

            </FieldArray>



            <button
              disabled={!isValid || !dirty}
              onClick={handleSubmit}
              type={`submit`}
            >Отправить</button>
          </div>
        )}
      </Formik>
    </div>
  );
}

export default App;

В деталях

Рассматривается две темы:

  1. Изменение схемы валидации в зависимости от данных формы.
  2. Работа с файлами в Formik и их валидация в yup

Изменение схемы Yup в зависимости от данных формы

Для изменения схемы валидации в yup есть метод when. Он доступен для всех типов схем yup (string, number, array, mixed, …). Метод when принимает 2 аргумента: ключ поля или массив ключей полей, значения которых влияют на схемы валидации; «билдер», состоящий (в основном) из двух полей «is» и «then». Поле «is» должно содержать значение или функцию. Если оно содержит значение, то нужно вставить такое значение поля, на которое мы ссылаемся, которое нам нужно, чтобы применить новую схему. Если это функция, то она просто должна вернуть true. Аргументами в ней будут значения из формы, ключи которых мы перечислим в первом аргументе метода when . Поле «then» — это схема, по которой будет валидироваться поле в случае если «is» вернёт true.

const validationsSchema = yup.object().shape({
    addressRegister: yup.string().required(),
    likeRegister: yup.bool(),
    addressActual: yup.string().when('likeRegister', {
      is: false,
      then: yup.string().required()
    })})

Выше приведён код, который читается следующим образом: есть поле «likeRegister», которое обозначает, что фактический адрес совпадает с юридическим. Если адреса совпадают, то поле «addressActual» не обязательно, а если же не совпадают (likeRegister === false), то поле становится обязательным.

Работа с файлами в Formik вместе с Yup

Я уже рассматривал как можно работать с файлами в Formik и озвучил несколько решений проблем, которые есть в этой связке в статье «Создание FileList. Передаём FileList в input ref React«. Там есть подробный код с созданием компонента с файловым инпутом.

Для понимания этого кода нужно уяснить пару вещей:

  1. Инпут содержит FileList (files) — массивоподобную структуру
  2. Для работы с массивами в Formik используется FieldArray
  3. Значение такого инпута лучше хранить в виде объекта, так легче осуществлять валидации
  4. Валидируйте формат файла через mime-type, а не через расширение. Это более надёжно.

Валидация mime-type

const avaliableMimeType = ['application/pdf', 'image/jpeg', 'image/png']
// Внутри схемы
type: yup.string().oneOf(avaliableMimeType, 'Недопустимый тип файла')

В данном примере мы разрешили принимать pdf, jpeg/jpg и png. Больше mime-type вы можете найти в интернете, например в википедии