Создание FileList. Передаём FileList в input ref React

Йо-йо! Уже довольно продолжительно я работаю фронтендером. В одном из проектов столкнулся с довольно частой задачей — создание списков файлов для передачи в input.

Такая необходимость возникла из-за работы с библиотекой Formik. Дело в том, что формик работает через так называемый FieldArray, который воспринимает значения как массив и с помощью методов arrayHelpers работает с данными.

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

Создание FileList

Для создания файл-листа нам нужно немного кода. В js нет интерфейса для создания FileList, но есть DataTransfer. Итак код:

const dt = new DataTransfer()
dt.items.add(ВАШ ФАЙЛ)
// Ваш FileList
const fileList = dt.files

Если бы у вас был массив фалов, то вы бы могли сделать так:

const dt = new DataTransfer() 
arrFiles.forEach((file) => { 
      dt.items.add(file)
})
const fileList = dt.files

Ну или вообще можно сделать пустой FileList

const dt = new DataTransfer() 
arrFiles.forEach((file) => { 
      dt.items.add(file)
})
const fileList = dt.files

Как сделать из FileList Array

Можно сделать так:

Array.from(FileList)

Ещё вы можете просто пройтись циклом for

for (var i = 0; i < files.length; i++) {
    // получаем сам файл
    file = files.item(i);
    // или можно так
    file = files[i];
}

Ссылка на MDN

Рабочий пример fiel input’а с Formik и yup

import React, { useRef } from 'react';
import { FieldArray, Formik } from 'formik'
import * as yup from 'yup'

const App = () => {

  const inputRef = useRef()

  /**
   * Получаем массив файлов из FileList
   * @param {*} fileList 
   */
  const getFileArray = (fileList) => {
    return Array.from(fileList)
  }

  /**
   * Хелпер для установки занчения в формике
   * пример будет актуален для инпутов с одним файлом
   * для множественных значений нужно будет переработать
   * @param {*} event 
   * @param {*} values 
   * @param {*} arrayHelper 
   */
  const handleFileChange = (event, values, arrayHelper) => {
    const arrFiles = getFileArray(event.target.files)
    const file = arrFiles.length ? arrFiles[0] : undefined

    if (!file) return
    // Проверяем есть ли значения
    if (Array.isArray(values)) {
      /**
       * Заменяем значение по индексу 0 на наш новый файл
       * обратите внимение что мы вставляем объект { file: File }
       * так будет лечге валидировать
       * если нужно будет валифировать другие свойства файла то расширьте объект
       * и схему валидации 
       * @example { file: File, type: File.type }
       * 
       * для варианта с множеством файлом предлагаю вам вынести часть
       * кода с <FieldArray> в отдельный компонент, при передачи пропров 
       * компонент будет рендериться зановов и вы будете всегда работать 
       * с чистым arrayHelper и только всегда пушить
       */
      arrayHelper.replace(0, { file })
    } else {
      // Или вставляем если ещё нет ни одного значения
      arrayHelper.push({ file })
    }

    /**
     * Создаём новый fileList, хотя от того можно отказаться
     * вставляем его в наш ref
     */
    const dt = new DataTransfer()
    arrFiles.forEach((file) => {
      dt.items.add(file)
    })

    inputRef.current.files = dt.files
  }

  /**
   * Дополнительное действие при очистке формы
   * без него может не срабатывать onChange
   */
  const handleDelete = () => {
    const dt = new DataTransfer()
    // Тут вставляем пустой FileList
    inputRef.current.files = dt.files
  }

  /**
   * Валидационная схема
   * Если вы будете расширять схему валидации для файла 
   * то начните после file: yup..., ВАШ КОД ДАЛЕЕ
   */
  const validationShema = yup.object().shape({
    fileInput: yup.array().of(yup.object().shape({
      // test('НАЗВАНИЕ ОШИБКИ', 'ОПИСАНИЕ ОШИБКИ', Функция проверки)
      file: yup.mixed().test('fileSize', 'fileSize', (value) => value ? value.size < 1 : false),
    }))
  })


  /**
   * Функция для печати ошибок
   * для ошибок массивов в errors будет обхект 
   * типа { [НАЗВАНИЕ ОШИБКИ]: ОПИСАНИЕ ОШИБКИ }
   * @param {*} errors 
   */
  const printErrors = (errors) => {
    if (Array.isArray(errors)) {
      return errors.map((obj) => Object.values(obj).map((error) => <p key={error}>{error}</p>))
    }
    if (typeof errors === 'string') {
      return errors
    }
    return null
  }

  return (
    <div>
      <Formik
        initialValues={{}}
        validateOnChange
        validateOnBlur
        validateOnMount
        onSubmit={(values) => console.log(values)}
        validationSchema={validationShema}
      >
        {({ values, errors, isValid, handleSubmit, handleReset }) => (
          <>
            <label htmlFor={`fileInput`}>
              Загрузить файл
            </label>
            {
              /**
               * fileInput - такое же как и у инпута с которым работаем
               */
            }
            <FieldArray
              name={`fileInput`}
              render={arrayHelper => (
                <>
                  <input
                    ref={inputRef}
                    name={`fileInput`}
                    type={`file`}
                    accept={`.pdf`}
                    onChange={(event) => {
                      handleFileChange(event, values.fileInput, arrayHelper)
                    }}
                  />
                  {printErrors(errors.fileInput)}
                </>
              )}
            />
            <div>
              <button disabled={!isValid} onClick={handleSubmit} type={`submit`}>Отправить</button>
            </div>
            <button onClick={(event) => {
              handleReset(event)
              // Обязательно наш хелпер для удаления (переачи нового FileList в input)
              handleDelete()
            }} type={`submit`}>Отчистить</button>
          </>
        )}
      </Formik>
    </div>
  );
}

export default App;

Для тех кто хочет поэксперементировать я создал репозиторий, переходите по ссылке