Задача: отправка и обработка файлов с помощью 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) Можем использовать наш отправщик, не забыв поднять локальный сервер:
ссылка на демо
ссылка на оригинал статьи https://habrahabr.ru/post/325340/
Добавить комментарий