Улучшаем опыт взаимодействия с формами

Улучшаем опыт взаимодействия с формами Часто меня спрашивают студенты: «Какой элемент сайта самый важный?», на что я им отвечаю — формы. Ведь с помощью форм пользователи совершают почти все конверсионные действия. Именно с этим элементом связано больше всего проблем. В этой статье я постараюсь рассказать, что можно улучшить при взаимодействии с формами. А заодно описать новые возможности работы с ними в браузерах.

Однако, сначала я бы хотел обозначить свою позицию по разработке таких форм. По-моему мнению, правильным подходом при разработке интерфейсов является подход прогрессивного улучшения.

Пример прогрессивного улучшения
Отличная иллюстрация Progressive Enhancement из презентации Анны Селезнёвой.

Александр Першин подробно рассказывал об этом подходе в своей статье Progressive Enhancement. Если вы до сих пор не сталкивались с ним, то я вам крайне рекомендую прочитать.

Ещё один нюанс: я не считаю, что интерфейс должен работать и выглядеть одинаково во всех браузерах. Я твёрдо уверен в том, что не стоит мучить пользователей со старыми браузерами вашими многокилобайтными библиотеками — им и без этого плохо.

Браузерная статистика рунета

Так как я буду показывать подходы, использующие новые возможности браузеров, взглянем на текущую статистику рунета. Каждый сможет для себя определить степень готовности пользователей. Я разделил её на две категории: полная и частичная поддержка.

Все приёмы в этой статье оценят 56,8% пользователей. В этой статистике присутствуют браузеры: IE 10, Firefox 11-17, Chrome 4-24, Safari 6, Opera 12.

Часть приёмов оценят 80,1% пользователей. Сюда дополнительно включил поддержку: IE 8-9, Safari 4-5, Opera 10-11.

Мобильные браузеры в статистику не включал, хотя они бы дали ещё больший процент поддержки. Информацию брал из LiveInternet со срезом по России. В расчёт попал средний трафик за 3 месяца (октябрь-декабрь) 2012 года.

Новые атрибуты форм в HTML5

Внимание: примеры в этой статье не содержат jQuery кода. Весь JavaScript написан с помощью фреймворка Vanilla JS.

Для начала, напомню о новых атрибутах у полей формы, которые буду использовать: required, autofocus, placeholder.

  • required — обязательное поле для заполнения;
  • autofocus — установка фокуса на поле при загрузке страницы;
  • placeholder — описание поля.
Поддержка новых атрибутов форм.
Поддержка в Internet Explorer версии 10
10
Поддержка в Firefox версии 4
4
Поддержка в Chrome версии 6
6
Поддержка в Safari версии 5
5
Поддержка в Opera версии 10
10

Вместе с этим появилось много новых типов полей: date, email, number, range и другие. Однако, самое безобидное из них (email), до сих пор используется с опаской. А ведь для того, чтобы оно заработало специальных действий не нужно. Браузеры, которые не знают этот тип полей, будут считать его текстовым.

Также появились дополнительные селекторы в CSS: E:valid, E:invalid, E:required — с помощью которых можно описывать стилевое оформление полей в разных ситуациях.

Демонстрация работы новых атрибутов полей и их оформления.

Такой подход, конечно, не будет работать в старых браузерах. Однако, так как мы прогрессивно улучшаем форму, нас это не должно сильно беспокоить. Форма остаётся работать даже в старых браузерах. В любом случае, проверка введённых данных должна происходить на серверной стороне. Возьмите себе за правило не доверять пользовательским данным и всегда полностью проверять их на сервере.

Запись данных формы по мере ввода

Поддержка localStorage.
Поддержка в Internet Explorer версии 8
8
Поддержка в Firefox версии 3.5
3.5
Поддержка в Chrome версии 4
4
Поддержка в Safari версии 4
4
Поддержка в Opera версии 10.5
10.5

Одна из частых проблем заполнения форм — введённые данные теряются. Это может произойти по разным причинам: ошибка сайта, переход по ссылке, в конце концов, может отключиться интернет. Решить эту проблему можно по-разному, например, записывать данные в localStorage по мере ввода.

if (window.localStorage) {
  var elements = document.querySelectorAll('[name]');

  for (var i = 0, length = elements.length; i < length; i++) {
    (function(element) {
      var name = element.getAttribute('name');

      element.value = localStorage.getItem(name) || '';

      element.onkeyup = function() {
        localStorage.setItem(name, element.value);
      };
    })(elements[i]);
  }
}

Валидация формы и отправка данных аяксом

Раз уж мы используем атрибуты required, то можно и валидацию сделать по-новому. В спецификации HTML5 для элемента формы добавлен метод checkValidity(). Он возвращает true или false. Стратегия работы формы будет очень простой: если проверка валидации даёт отрицательный результат — мы блокируем отправку формы, в обратном случае — разрешаем отправку.

submit.disabled = !form.checkValidity();
Поддержка FormData из XHR2.
Поддержка в Internet Explorer версии 10
10
Поддержка в Firefox версии 4
4
Поддержка в Chrome версии 7
7
Поддержка в Safari версии 5
5
Поддержка в Opera версии 12
12

Теперь добавим возможность отправлять форму без перезагрузки, с помощью аякс. Со второй версией спецификации XMLHttpRequest мы получили много интересного. Например, мы можем больше не заниматься сбором данных для отправки формы, для этого есть объект FormData.

form.onsubmit = function(event) {
  if (window.FormData) {
    event.preventDefault();

    var data = new FormData(form);
    var xhr = new XMLHttpRequest();
    var url = form.getAttribute('action') + '?time=' + (new Date()).getTime();

    xhr.open('post', url);
    xhr.onreadystatechange = function() {
      if (xhr.readyState == 4 && xhr.status == 200) {
        // server response: xhr.responseText
      }
    };
    xhr.send(data);
  }
};

При работе с асинхронными запросами следует помнить, что некоторые браузеры кэшируют результат. Например, это делает Internet Explorer, Mobile Safari (iOS 6) и другие. Чтобы избежать эту проблему, можно добавлять к адресу запроса текущее время.

Сейчас ответ от сервера мы получаем в текстовом виде (xhr.responseText), но со временем это изменится. Например, если мы точно знаем, что ответом будет JSON, мы можем получить JavaScript объект сразу.

var xhr = new XMLHttpRequest();

xhr.open(method, url);
xhr.responseType = 'json';

xhr.onreadystatechange = function() {
  if (xhr.readyState == 4 && xhr.status == 200) {
    // server response: xhr.response
  }
};

xhr.send();

Обратите внимание, что ответ сервера будет в свойстве xhr.response. А свойство xhr.responseType может принимать и другие значения, например: arraybuffer, blob, document.

Демонстрация сохранения данных формы и отправки их с помощью XMLHttpRequest.

После успешной отправки формы я советую оставить контактную информацию в localStorage, а остальное очистить. Таким образом, если пользователь захочет ещё раз отправить форму, часть информации уже будет заполнена.

Предварительный просмотр закачиваемых фотографий

Плавно переходим на работу с файлами в формах. До недавнего времени почти никаких средств для работы с файлами не было. Но всё меняется. Начнём с простого — новые атрибуты для полей загрузки файлов.

  • multiple — позволяет выбирать несколько файлов;
  • accept — даёт возможность указывать, какие файлы выбирать.

Допустим, мы хотим к нашей форме добавить возможность закачивать несколько фотографий. Такое поле будет выглядеть как-то так.

<input type="file" name="image" accept="image/*" multiple>

Хочу напомнить: поле с такими атрибутами будет работать в старых браузерах. Ограничением будет:

  1. Только один файл;
  2. Валидация файлов производится на серверной стороне.
Поддержка FileReader из File API.
Поддержка в Internet Explorer версии 10
10
Поддержка в Firefox версии 3.6
3.6
Поддержка в Chrome версии 6
6
Поддержка в Safari версии 6
6
Поддержка в Opera версии 11.10
11.10

Попробуем улучшить опыт взаимодействия с файлами. Раз мы ожидаем от пользователей добавления фотографий, логично сделать возможным предварительный просмотр. Для этого мы будем использовать объект FileReader из спецификации File API.

document.querySelector('[type=file]').addEventListener('change', function() {
  [].forEach.call(this.files, function(file) {
    if (file.type.match(/image.*/)) {
      var reader = new FileReader();

      reader.onload = function(event) {
        var img = document.createElement('img');
        img.src = event.target.result;

        div.appendChild(img);

        queue.push({file: file, element: img});
      };

      reader.readAsDataURL(file);
    }
  });
}, false);

Таким образом, все выбранные фотографии мы сразу же отображаем на сайте.

Предварительный просмотр фотографий (FileReader)

А для того, чтобы отправить их с помощью аякса, мы собираем их в массиве queue. Ранее в статье мы использовали объект FormData, сейчас мы просто добавим к нему список файлов.

var data = new FormData(form);

queue.forEach(function(element) {
  data.append('image', element.file);
});

Только и всего, остальное остаётся таким же. Форма будет отправлена с файлами без перезагрузки.

Демонстрация предварительного просмотра фотографий и отправки их с помощью аякс.

Drag and drop файлов

Поддержка drag and drop файлов.
Поддержка в Internet Explorer версии 10
10
Поддержка в Firefox версии 3.5
3.5
Поддержка в Chrome версии 4
4
Поддержка в Safari версии 3.1
3.1
Поддержка в Opera версии 12
12

Попробуем уделить больше внимания файлам. Добавим возможность перетаскивать файлы с компьютера сразу в форму. При этом логика предварительного просмотра и отправки без перезагрузки должна остаться. Для начала давайте выделим работу с предварительным просмотром в отдельную функцию.

function previewImages(files) {
  [].forEach.call(files, function(file) {
    if (file.type.match(/image.*/)) {
      var reader = new FileReader();

      reader.onload = function(event) {
        var img = document.createElement('img');
        img.src = event.target.result;

        div.appendChild(img);

        queue.push({file: file, element: img});
      };

      reader.readAsDataURL(file);
    }
  });
}

Допустим, зоной для перемещения файлов будет блок с классом wrapper. Добавим события для него.

var file = document.querySelector('[type=file]');
var dropzone = document.querySelector('.wrapper');

file.addEventListener('change', function() {
  previewImages(this.files);
  this.value = '';
}, false);

dropzone.addEventListener('dragover', function(event) {
  event.preventDefault();

  dropzone.classList.add('active');
  event.dataTransfer.dropEffect = 'copy';
}, false);

dropzone.addEventListener('drop', function(event) {
  event.preventDefault();

  dropzone.classList.remove('active');
  previewImages(event.dataTransfer.files);
}, false);

Как видите, мы добавили события начала (dragover) и конца (drop) перемещения файлов. Все перемещённые файлы мы передаём функции previewImages. Таким образом, наша форма работает одинаково с файлами выбранными через сайт и перемещёнными с компьютера.

Процесс загрузки файлов (progress bar)

Фотографии бывают очень большими, поэтому попробуем отобразить процесс загрузки. Для визуализации этого процесса я возьму элемент progress, а вы можете взять div с двигающимся фоном. Сам процесс будет происходить в событии progress из спецификации XMLHttpRequest.

var xhr = new XMLHttpRequest();

xhr.upload.addEventListener('progress', function(event) {
  if (event.lengthComputable) {
    progress.value = Math.round((event.loaded * 100) / event.total);
  }
}, false);

Демонстрация drag & drop и прогресса загрузки файлов.

В итоге

Наша простая форма имеет ряд значительных улучшений в области UX.

  1. Валидация происходит в момент ввода;
  2. Введённые данные запоминаются пока не будут отправлены;
  3. Контактные данные сохраняются для повторной работы;
  4. Предварительный просмотр фотографий;
  5. Процесс загрузки файлов.

При этом, так как мы действовали в соответствии с прогрессивным улучшением, наша форма работает везде:

  • в IE 6-7 и других старых браузерах,
  • с отключённым JavaScript,
  • на мобильных устройствах.
Загрузка фотографий с мобильного телефона
Работа FileReader на iOS 6.

Уверен, занимаясь улучшением UX форм, можно найти более интересные решения. Пожалуйста, добавьте ваши решения, советы, критику в комментариях ниже. Спасибо!

Что ещё почитать?

← Что мне дал 2012 годCSS transition и animation →