Привет, Хабр!

Хочу поделиться с общественностью своей небольшой (всего 6 Кбайт) js-библиотекой, которая сильно облегчает мне работу с формами при разработке сайтов, и позволяет сократить написание кода.
В любом, более-менее среднем и крупном проекте насчитается не один десяток форм, многие из которых желательно отправлять через AJAX. Часто для этого просто вешается обработчик и пишется скрипт, что-то типа этого:

$(function () {
    $('.contact-submit').click(function () {
        $.ajax({
            url: '/path/to/action',
            method: 'post',
            data: $(this).closest('form').serialize(),
            success: function () {
                /* success action */
            }
        });
    });
});

И вроде всё работает, но, к сожалению, очень часто разработчики забывают мелочи, такие как:
  • отсутствие индикации отправки формы;
  • как следствие, возможность накликать множество запросов (повторная отправка не блокируется);
  • невозможность отправки формы с клавиатуры;
  • отсутствие индикации при ошибке отправки формы (например, сервер ответил 500-ой ошибкой);
  • хардкод url контроллера в js-скрипте.

Кроме того, есть множество тонкостей, которые не учтены в большинстве аналогичных библиотек, такие как: передача name=value кнопки, осуществляющей отправку формы, поддержка <input type="image"> и атрибутов form, formaction, formmethod.
А учитывая то, что форм в крупных проектах достаточно много, то такие обработчики для каждого типа форм в результате образуют весомый объём кода. А ведь часто, формы достаточно простые, такие как «форма обратной связи», нам нужно всего лишь отправить форму и показать статус — отправлено письмо или возникла ошибка. С помощью описываемой в данной статье библиотеки можно вовсе не писать код для таких простых форм.
Приступлю к описанию возможностей.

Поддержка html5 атрибутов для старых браузеров


Благодаря атрибутам form, formaction, formmethod, formenctype, formtarget можно гибко изменять поведения отправки формы. Самый часто встречающийся пример — выбор операции над множеством объектов, выбираемых пользователем с помощью checkbox:
Код примера
<div class="items">
    <div class="item">
        <input type="checkbox" form="mass"> Выбрать
        <form action="/cart/add">
            <input type="text" value="1">
            <button type="submit">Добавить в корзину</button>
        </form>
    </div>
    
    <div class="item">
        <input type="checkbox" form="mass"> Выбрать
        <form action="/cart/add">
            <input type="text" value="1">
            <button type="submit">Добавить в корзину</button>
        </form>
    </div>
    
    ...
    
    <form id="mass" method="post">
        <button type="submit" formaction="/controller/move">Переместить</button>
        <button type="submit" formaction="/controller/delete">Удалить</button>
    </form>
</div>


С пощью атрибутов form и formaction мы смогли решить проблему невозможности вложенных форм, а также выбор url контроллера в зависимости от действия.
Библиотека предоставляет polyfill для браузеров, которые не поддерживают данные атрибуты (IE<10).

Демо

Блокировка повторной отправки и отображение состояния


Думаю, с этим встречались многие не раз, что пользователь нажимает кнопку отправки формы несколько раз, и в результате вместо, например, одного комментария, публикуются сразу три одинаковых. Очень часто, чтобы избежать подобного эффекта, просто пишут скрипт, который делает кнопку отправки disabled после нажатия, забывая при этом, что форму можно ещё отправить с клавиатуры.
Библиотека также добавляет класс form-loading когда происходит процесс отправки, благодаря этому можно с помощью CSS застилить это состояние формы так, как захочется.
Если всё же требуется возможность повторной отправки формы до завершения предыдущей, то достаточно добавить класс form-no-block.

Демо

Фильтрация пустых полей при отправке


Данная возможность может пригодиться для форм поиска/фильтрации товара, в которых много полей, для того, чтобы URL выглядели более наглядно. Согласитесь, что увидеть в адресной строке лучше
http://example.com/?price_to=1000
чем
http://example.com/?price_to=1000&price_to=&amount_from=&amount_to=
Просто добавьте класс form-no-empty к форме.

Демо

AJAX отправка формы


Это, пожалуй, одна из главных функций библиотеки. Отправлять формы через AJAX умеют многие плагины, а вот отправлять форму также, как это делает браузер — к сожалению, нет. В частности, в ней реализована поддержка описываемых выше html5 атрибутов form, formaction, formmethod, передача параметров активной submit-кнопки и координат для <input type="image">. Для того, чтобы передача формы осуществлялась через AJAX надо добавить CSS-класс form-ajax к форме, а для отображения результата — добавить в произвольное место внутри формы тег <output> или другой тег с классом form-output. Можно указать элемент вне формы, для этого в атрибуте data-form-output указать jQuery-селектор этого элемента. Аналогичный атрибут можно использовать и в корневом элементе ответа (он более приоритетный). А если корневой элемент ответа имеет CSS-класс form-replace, то в качестве элемента вывода будет использована сама форма, причём она заменится, что очень удобно для «одноразовых» форм. Рассмотрим это на примере формы обратной связи:
Код формы обратной связи
<form action="/site/callback" method="post" class="form-ajax">
    <div class="form-group">
        <label>Имя</label>
        <input type="text" name="name">
    </div>
    <div class="form-group">
        <label>Email</label>
        <input type="text" name="email">
    </div>
    <div class="form-group">
        <label>Сообщение</label>
        <textarea name="message"></textarea>
    </div>
    <output></output>
    <button type="submit">Отправить</button>
</form>


// контроллер на примере Yii 2
public function actionCallback() {
    $model = new CallbackForm();
    $model->load(Yii::$app->request->post())
    if ($model->validate()) {
        // тут отправляем письмо, успех статус в $success
        if ($success) {
            return '<div class="alert alert-success form-replace">Сообщение отправлено</div>';
        }
    }
    return '<div class="alert alert-danger">Ошибка отправки</div>';
}

Алгоритм такой — форма отправляется через AJAX, и если она проходит валидацию и ошибок в отправки нет, то выводим Bootstrap Alert что всё в порядке, причём он заменит собой форму. Если же есть какие-то ошибки, выводим их в тег <output>. И заметьте, нет ни строчки JavaScript. Библиотека имеет поддержку Bootstrap Alert, так, что он автоматически делает alert-dismissible и самостоятельно добавляет кнопку закрытия.

Для AJAX-форм генерируются следующие события:
  • formajaxbefore — перед отправкой формы, позволяет изменить все настройки $.ajax;
  • formajaxdone — в случае успешной отправки;
  • formajaxfail — в случае возникновения ошибки;
  • formajaxalways — в любом случае, после завершения AJAX-запроса.

В целом, это тоже облегчает написание своих скриптов, т. к. позволяет навешивать код прямо на событие успешного ответа, минуя рутину с передачей URL и параметров. В событиях можно отменить поведение скрипта по-умолчанию, достаточно вызвать e.preventDefault() — никаких манипуляций с выводом контента не будет происходить.
Если же, наоборот, подгружаемый контент нужен и, более того, содержит другие компоненты, которые надо проинициализировать (например, datepicker). Для этого библиотека генерирует ещё два события:
  • contentprepare — до добавления контента в DOM;
  • contentinit — после добавления контента в DOM.

Для себя я сделал вывод, что мне проще иметь одинаковую процедуру инициализации всех плагинов, поэтому пришёл к такому процессу:
$(function () {
    $(document).trigger('contentinit', [$(document)]);
});
$(document).on('contentinit', function (e, cont) {
    cont.find('.datepicker').datepicker();
});

То есть при загрузке страницы мы также генерируем событие contentinit, на который навешиваем обработчик, инициализирующий все плагины.

Демо

AJAX отправка файлов


Отправить без перезагрузки страницы форму с файлами ещё сложнее, т. к. устаревшие браузеры этого вообще не умеют, и тут нужен iframe-метод для эмуляции этого процесса. Здесь я не стал изобретать велосипед, и отдал данный функционал полностью на плечи malsup jquery.form плагину, оставив лишь единый способ его отправки. На стороне сервера всё выглядит также, как если бы вы отправляли формы стандартным способом.
Чтобы отправить форму с файлами, необходимо подключить malsup jquery.form плагин и указать enctype=«multipart/form-data». Для пересылаемой данным способом формы дополнительно будет генерироваться событие:
  • formajaxprogress — событие, информируещее о текущем состоянии отправки.

Демо

AJAX редирект


Ещё одна небольшая полезная мелочь. Чтобы заставить страницу перейти на новую (осуществить редирект), просто укажите заголовок X-Redirect, значение которого — URL для перехода.

Демо

Кнопка, для классического субмита в AJAX-форме


Можно сделать так, что в AJAX-форме будет одна или несколько кнопок, которые отправляют форму обычным способом. Например, я это использую на странице корзины — когда пользователь меняет количество товара, то применяется AJAX для сохранения изменений. Когда же он нажимает кнопку «Перейти к оформлению», происходит обычная отправка той же формы.

Демо

Изменение внешнего вида кнопки отправки формы


Библиотека предоставляет ещё несколько хелперов для того, чтобы изменить вид кнопки отправки для того, чтобы пользователи понимали, что форма в процессе отправки. Для этого добавьте к кнопке CSS-класс btn-loading, а также используйте одну или обе возможности:
  1. aтрибут data-loading-text — меняет текст кнопки во время отправки формы на указанный в атрибуте, а после завершения, возвращает обратно;
  2. aтрибут data-loading-icon — добавляет к кнопке значок, показывающий процесс загрузки.

Например:
<!-- font awesome -->
<button type="submit" class="btn btn-primary btn-loading" data-loading-icon="fa fa-refresh fa-spin">Submit</button>

<!-- bootstrap glyph -->
<button type="submit" class="btn btn-primary btn-loading" data-loading-icon="glyphicon glyphicon-refresh">Submit</button>

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

Демо

Алерты для форм


Иногда бывает нужно, что надо отобразить пользователю какие-то предупреждения или сообщения об ошибках в форме, например, об ошибках валидации. Библиотека предоставляет простой интерфейс для добавления bootstrap alert-ов в элемент вывода формы. Используется так:
$('form').formAlert('Ошибка! Заполните <b>обязательные поля</b>!', 'danger');
$('form').formAlert('#external-alert', 'success', true);

Демо

Заключение


Библиотека доступна в репозитариях npm и bower под наименованием paulzi-form:
bower install paulzi-form
npm install paulzi-form

Она достаточно молодая, поэтому жду отчётов об ошибках на гитхабе. Буду рад, если кому-то скрипт тоже облегчит жизнь, как и мне.

Проект на GitHub

Комментарии (17)


  1. Magistr1976
    29.06.2015 07:57

    То что надо!


  1. shamanis
    29.06.2015 08:39
    +16

    Половина «проблем» решается, если обработчик вешать не на click, а все-таки на submit.


    1. DeadikGudwin
      29.06.2015 09:43
      +4

      а отправка файлов решается использованием formData вместо сериализации


      1. PaulZi Автор
        29.06.2015 10:04

        FormData доступен начиная с XMLHttpRequest 2. Никто не спорит что всё это можно решить руками, библиотека лишь предоставляет удобный способ вообще не писать скрипты для этого, и не думать как это будет работать, если форма передаётся через AJAX.


    1. firya
      29.06.2015 12:58
      +1

      а «хардкод url контроллера в js-скрипте» решается, например так:

      url: form.attr("action"),
      


  1. vsb
    29.06.2015 09:40

    интересно, почему X-Redirect, а не стандартный HTTP 302 и заголовок Location?


    1. PaulZi Автор
      29.06.2015 10:00
      +2

      Потому что из JS запрещено читать некоторые стандартные заголовки, если не указаны CORS. Кроме того может потребоваться редирект именно AJAX-запроса, а не всей страницы.


  1. past
    29.06.2015 09:57
    -1

    Что на счет websockets?


    1. PaulZi Автор
      29.06.2015 10:07
      +1

      Не понял, как и для чего вы хотите применить здесь WebSockets?


      1. past
        29.06.2015 11:13

        Вместо XMLHttpRequest.
        Как транспорт для общения с сервером.


        1. PaulZi Автор
          29.06.2015 11:20
          +1

          А что мы от этого выигрываем? Поддержка браузерами веб-сокетов ничуть не лучше даже чем XMLHttpRequest 2. Если же нужна постоянная двусторонняя связь с сервером, а не отправка формы, для чего WebSockets и создавались, то это уже совсем другая задача.


          1. past
            29.06.2015 11:23

            Понял Вас, спасибо!


          1. Xao
            29.06.2015 12:25

            Вроде же, начиная с IE8 всё поддерживается. Вдогонку, там всякие оперы и фаерфоксы тех времён, хром и подавно.


            1. AlexP11223
              29.06.2015 12:51

              1. Xao
                30.06.2015 09:23
                -1

                Я имел ввиду XMLHttpRequest 2. С вебсокетами всё намноооооого хуже.



  1. dolphin4ik
    29.06.2015 10:14

    Запретить повторную отправку можно и кукисами из js, ну и на сервере ещё раз проверить