Форма загрузки файлов: 5 починок без редизайна
Запрос с вложениями, анкета кандидата, заявка на гарантию с фото из телефона: человек выбирает файл, нажимает «Отправить» и получает ответ «файл слишком большой». Пробует второй раз со скриншотом, поле очищается, вкладка закрывается. Форма загрузки файлов проверяется в эти две минуты, и чинится она дешевле всего остального в форме.
Код: Oat-Upload, лицензия MIT.
Oat Upload это небольшое расширение библиотеки Oat UI. Оно оборачивает нативный <input type="file"> и добавляет зону перетаскивания, превью, удаление, проверку и прогресс. Зависимостей в рантайме нет, сборки на сервере нет, форма отправляется, даже если скрипт не загрузился.
Где форма загрузки файлов теряет человека
Пять провалов повторяются из проекта в проект, и сама загрузка среди них не виновата.
Человек не видит, что прикрепил. Камера телефона называет файлы IMG_4821. Без превью не тот документ уезжает в поддержку, и через день его правят руками.
Про лимит сообщают после попытки. «До 5 МБ» мелким серым шрифтом под полем это подсказка, а подсказка не проверка. Компонент сравнивает размер до того, как запрос ушёл из браузера, поэтому отказ стоит секунды, а не круга до сервера и обратно.
Ошибка стирает выбор. Отклонённый файл с очисткой поля заставляет выбирать всё заново. Здесь отклонённый файл выпадает из списка, остальные остаются.
На медленной связи не происходит ничего. Без индикатора сорок секунд загрузки выглядят зависшей страницей, и человек перезагружает вкладку. Перезагрузка отменяет загрузку.
Удалить файл нельзя. Прикрепили не тот договор, и единственный выход это перезагрузка страницы, которая уносит остальную форму.
Поле остаётся источником истины
<ot-upload> не заменяет поле, а растёт вокруг него:
<ot-upload max-size="5242880" max-files="5">
<input type="file" name="attachments" multiple accept="image/*,.pdf">
</ot-upload>
Это вся интеграция. Форма отправляет attachments так же, как раньше, сервер держит свою проверку, а разработчик в шаблоне видит поле файла, а не фреймворк.
Список файлов на странице и список в браузере синхронизируются через DataTransfer там, где браузер это умеет. Превью и отправка не расходятся.
Проверка, которая говорит до запроса
Проверка читает сначала нативный input, потом атрибуты <ot-upload>, поэтому accept, multiple и disabled ведут себя так, как определил браузер. max-size и max-files закрывают два лимита, которые браузер сам не держит.
Отклонённый файл вызывает ot-upload-error с причиной type, size, count или disabled и сообщением для человека. Из этой причины форма собирает «PDF весит 12 МБ, лимит 5 МБ» вместо «недопустимый файл».
Прогресс и удаление без спиннера
Прогресс это нативный <progress>, который обновляет setProgress(). Код загрузки передаёт число от 0 до 100, и полоса показывает реальное положение дел, а не анимацию. Вызов с null или resetProgress() прячет её.
Удаление это кнопка в строке, а remove() принимает индекс, файл или идентификатор элемента. Поле обновляется вместе со списком, поэтому на сервер уходит то, что человек видит.
Для команд со своей разметкой части названы явно: data-upload-dropzone, data-upload-list, data-upload-error, data-upload-progress, data-upload-status. Компонент заполняет то, что дали, и собирает остальное, если не дали ничего.
Где это не подходит
Загрузка видео на 2 ГБ, докачка после обрыва сети и передача кусками в S3 это другая задача, и компонент вокруг нативного поля её не решает. Десять тысяч файлов в одном запросе тоже другая задача, и ответ там это очередь на сервере и другой интерфейс.
Для формы с пятью вложениями рамка верная, и страница остаётся страницей.
Сколько это стоит
Одна таблица стилей, один скрипт, MIT, без зависимостей. Для проекта по 30–50 $ в час это утро работы, и оно убирает класс обращений в поддержку, которые никто не выставляет в счёт: обращения про файлы.
Рядом: веб-разработка о том, как считается объём формы, и сортировка таблицы HTML о том же доводе в пользу нативной разметки вместо её замены.
