Задача: отправка и обработка файлов с помощью FormData и FileReader в форме со всеми возможными полями и пересылкой дополнительных параметров для каждого поля c объединением всех данных формы (кроме файлов и системных полей) в общий массив.
Поддержка: все современные браузеры, IE 10+.
Плагины: jquery-2.1.4
Для начала разберемся, что же такое FormData
Formdata — тип данных в рамках технологии XHR2, данные в нем хранятся в виде пар ключ / значение.
new Formdata () — это конструктор для создания объекта FormData.
FormData имеет множество методов для полноценной работы с ней, таких как:
- .get() — возвращает данные по ключу;
- .getAll() — возвращает массив всех значений, ассоциированных с этим ключом;
- .has() — возвращает булевое значение касательно наличия объекта;
- .set() — добавляет значение к уже существующему ключу и, если его нет, создает его;
- .append() — создает новую пару ключ / значение;
- .delete() — удаляет объект по ключу;
- .forEach() — на нем остановимся подробнее:
В начале работы с FormData появилась весьма сложная проблема из-за того что встал вопрос: как можно перебрать данные в этом объекте? На русскоязычных ресурсах данных найдено не было, зато при получении списка всех методов объекта был найден forEach(), который позволил очень легко перебирать данные. Но появилась проблема, связанная с поддержкой браузерами. Так что этот метод не годится — нужна полная поддержка.
Также FormData можно перебирать с помощью цикла for...of (доступно в ECMAScript 6, с нативной поддержкой которого также есть проблемы).
Главная проблема FormData заключается в Internet explorer (как всегда), а вернее, в его поддержке. Из всех методов, которые есть в FormData, Internet explorer поддерживает только append(), что уничтожает всю простоту использования. Следовательно, мы не можем собрать форму с помощью простого вызова конструктора и последующего изменения данных в ней, и придется это делать вручную:
- Получим все данные формы через serializeArray(), переберем, проверим их на пустоту и, вместе с заголовком (data-title), если это не системное поле (type=”hidden”), занесем в ассоциативный массив, отдельный для каждого поля, а далее добавим в наш массив для данных формы.
- Системные поля мы сразу добавляем методом append() в FormData.
Файлы будем собирать с помощью списка, который формирует пользователь при закачке и дальнейшими манипуляциями со списком на клиенте, то есть будем сравнивать те файлы, которые у нас остались в списке, с теми, что хранятся в input type=”file” и с помощью переборки добавлять только те, что оставил пользователь.
Теперь познакомимся с FileReader
FileReader — это объект, который позволяет веб-приложениям асинхронно читать содержимое файлов (или буферы данных), хранящиеся на компьютере пользователя, используя объекты File или Blob, с помощью которых задается файл или данные для чтения.
С его помощью мы будем отслеживать загрузку файлов на клиенте, формировать список загруженных файлов и выводить для них прогресс бар.
Теперь к самой задаче
Форма, которую мы будет пересылать:
<form enctype="multipart/form-data" id="form">
<!-- Тема письма (служебное поле)-->
<input type="hidden" name="thm" data-title="Тема" value="Заполнить анкету">
<div class="radio-list">
<div class="radio">
<input type="radio" name="radiobtn" value="Первый" data-title="Выбор пунктов" id="radio1" class="radio__input" checked>
<label for="radio1" class="radio__label">Первый пункт</label>
</div>
<div class="radio">
<input type="radio" name="radiobtn" value="Второй" data-title="Выбор пунктов" id="radio2" class="radio__input">
<label for="radio2" class="radio__label">Второй пункт</label>
</div>
</div>
<div class="checkbox-list">
<div class="checkbox">
<input type="checkbox" name="checkboxbtn" value="Первый" data-title="Выбор пунктов2" id="checkbox1" class="checkbox__input" checked>
<label for="checkbox1" class="checkbox__label">Первый пункт</label>
</div>
<div class="checkbox">
<input type="checkbox" name="checkboxbtn" value="Второй" data-title="Выбор пунктов2" id="checkbox2" class="checkbox__input">
<label for="checkbox2" class="checkbox__label">Второй пункт</label>
</div>
</div>
<input type="text" name="name" data-title="Текстовое поле" class="input-text">
<textarea name="textarea" data-title="Сообщение" class="textarea"></textarea>
<!-- input для файла -->
<input class="input-file js_file_check" type="file" name="file[]" data-title="документ" multiple="" accept="image">
<!--Список файлов загруженных пользователем-->
<ul class="js_file_list file-list">
</ul>
<!-- кнопка для отправки формы-->
<button class="js_btn_submit">Отправка формы</button>
</form>
Для удобства пользователей предоставим им возможность добавления сразу большого количества файлов. С этой целью укажем в поле name значение file[] и атрибут multiple, с ограничением только картинки accept=«image».
Для пользователей также будем выводить список файлов, которые они загрузили с раздельным progress bar-ом для каждого файла и возможностью удаления перед отправкой. И тут мы столкнулись с проблемой. Дело в том, что fileList (массив загруженных файлов) у нашего input предназначен только для чтения, и удалить только выбранный пользователем файл мы не можем. Так что было решено перед отправкой на сервер сверять список, который уже сформировал пользователь, с тем что уже загружено. И при совпадении со списком файл будет добавляться в FormData.
1) Создаем саму функцию отправки через ajax:
var form = form; //текущая форма
function formSend(formObject, form) {
$.ajax({
type: "POST",
url: 'form-handler.php',
dataType: 'json',
contentType: false,
processData: false,
data: formObject,
success: function() {
$(form).trigger('reset');
//при успешной отправке сбрасываем форму в дефолтное состояние
alert('Success');
}
});
};
2) Создаем функцию сборки формы:
function formData_assembly(form) {
var formSendAll = new FormData(), //создаем объект FormData
form_arr = $(form).find(':input,select,textarea').serializeArray(), //собираем все данные с формы без файлов
formdata = {}; //ассациативный массив для хранения данных с формы
for (var i = 0; i < form_arr.length; i++) {
if (form_arr[i].value.length > 0) { //перебераем массив с данными формы и проверяем на заполненность
var current_input = $(form).find('input[name=' +
form_arr[i].name +
'],select[name=' +
form_arr[i].name +
'],textarea[name=' +
form_arr[i].name + ']'),
value_arr = {}; // новые массив с данными каждого поля + заголовок
var title = $(current_input).attr('data-title'); //заголовок поля
if ($(current_input).attr('type') != 'hidden') { //проверяем не является ли поле системным
value_arr['value'] = form_arr[i].value;
value_arr['title'] = title;
formdata[form_arr[i].name] = value_arr;
} else {
formSendAll.append(form_arr[i].name, form_arr[i].value); //системные поля пересылаем отдельно от общей формы
}
}
}
formdata = JSON.stringify(formdata);
formSendAll.append('formData', formdata); // добавляем все поля в formdata
// file
if ($(form).find('input[type=file]').hasClass('js_file_check')) { //проверяем есть ли input type file для пересылки
var current_input = $(form).find('input[type=file]');
if ($(current_input).val().length > 0) { //проверяем на заполненность
$('.js_file_list li').each(function() {
var list_file_name = $(this).find('span').text();
for (var k = 0; k < $(current_input)[0].files.length; k++) {
if (list_file_name == $(current_input)[0].files[k].name) { //сверяем список выбранных файлов для загрузки
formSendAll.append($(current_input).attr('name'), $(current_input)[0].files[k]); // добавляем только те что остались в списке
}
}
})
}
}
formSend(formSendAll, form);
}
formData_assembly(form);
3) Оборачиваем все это в функцию для удобного вызова по событию:
function submit_function(form){...}
4) Вешаем функцию на событие клика на кнопку отправки формы:
$('.js_btn_submit').click(function (e) {
e.preventDefault();
var current_form = $(this).closest('form');//Текущая форма
submit_function(current_form);
})
Теперь у нас есть полноценный отправщик формы, осталось только написать обработчик для файлов.
1) Создадим функцию отслеживания состояния input type=file:
function checkFile(){
var inputs = document.getElementsByClassName('js_file_check');
for (var i = 0; i < inputs.length; i++) {
inputs[i].addEventListener('change', handleFileSelect, false);
}
}
checkFile();
2) Напишем обработчик ошибок:
var reader;
function abortRead() {
reader.abort();
}
function errorHandler(evt) {
switch (evt.target.error.code) {
case evt.target.error.NOT_FOUND_ERR:
alert('File Not Found!');
break;
case evt.target.error.NOT_READABLE_ERR:
alert('File is not readable');
break;
case evt.target.error.ABORT_ERR:
break; // noop
default:
alert('An error occurred reading this file.');
};
}
3) Напишем функцию для переборки файлов в fileList нашего input type=file:
function handleFileSelect(evt) {
var thisInput = $(this); //input type file для множественных загрузок
for (var i = 0; i < thisInput[0].files.length; i++) { //перебираем все загруженные файлы и запускаем обработчик для каждого
reader_file(thisInput[0].files[i]); //добавляем обработчик для каждого файла
}
}
4) Теперь непосредственно сам обработчик:
function reader_file(file) {
var reader = new FileReader(),
fileName = file.name;
reader.onerror = errorHandler; //функция для обработки ошибок
$('.js_file_list').append('<li><span>' +
fileName +
'</span><div class="js_file_remove file_remove"></div><div class="progress-bar js_progress_bar"></div></li>'); //добавляем все новые файлы в список на клиенте
reader.onabort = function(e) {
alert('File read cancelled');
};
reader.onload = function(e) { //событие успешного окончания загрузки
//что-нибудь делаем
}
reader.onprogress = function(event) { // вывод процентной полосы загрузки
if (event.lengthComputable) {
var percent = parseInt(((event.loaded / event.total) * 100), 10);
$('.js_progress_bar').css('width', percent + '%');
}
}
if (reader.readAsBinaryString === undefined) { // если браузер не поддерживает readAsBinaryString
reader.readAsBinaryString = function(fileData) {
var binary = "",
pt = this,
reader = new FileReader();
reader.onload = function(e) {
var bytes = new Uint8Array(reader.result);
var length = bytes.byteLength;
for (var i = 0; i < length; i++) {
binary += String.fromCharCode(bytes[i]);
}
pt.content = binary;
$(pt).trigger('onload');
}
}
reader.readAsArrayBuffer(file);
} else {
reader.readAsBinaryString(file);
}
}
5) Добавим возможность удаления файлов из списка:
$(document).on('click', '.js_file_remove', function() {
var list_item = $(this).closest('li');
$(list_item).remove();
});
6) Можем использовать наш отправщик, не забыв поднять локальный сервер:
Автор: Mikle