Кроссбраузерная отправка формы с файлом или как переписать весь отправщик несколько раз после тестирования в IE

от автора

Задача: отправка и обработка файлов с помощью FormData и FileReader в форме со всеми возможными полями и пересылкой дополнительных параметров для каждого поля c объединением всех данных формы (кроме файлов и системных полей) в общий массив.

Поддержка: все современные браузеры, IE 10+.

Плагины: jquery-2.1.4

image

Для начала разберемся, что же такое FormData

Formdata — тип данных в рамках технологии XHR2, данные в нем хранятся в виде пар ключ / значение.
new Formdata () — это конструктор для создания объекта FormData.

Подробнее о FormData

image

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, с помощью которых задается файл или данные для чтения.

Подробнее о filereader

С его помощью мы будем отслеживать загрузку файлов на клиенте, формировать список загруженных файлов и выводить для них прогресс бар.

Теперь к самой задаче
Форма, которую мы будет пересылать:

<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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *