Тонкости свойства disable у кнопок формы, отправляемой на сервер (Как делать кнопки неактивными)

Уже неоднократно на хабре (вот в этой публикации и в этой) ставился вопрос о том, что было бы хорошо кнопкам формы, отправляемой на сервер, ставить свойство disabled = "disabled".

Однако, до сих пор так и не разобрались, зачем это нужно и как все-таки это делать. Казалось бы, что может быть проще и о чем здесь вообще можно разговаривать, ан нет — на поверку все оказалось не так тривиально. Сразу замечу, что нижеследующие рассуждения применимы к обеим типам форм: как отправляемым через обычный SUBMIT, так и с помощью AJAX.

Зачем нужно делать кнопки неактивными

  1. Чтобы пользователю стало очевидным, что он уже нажал на кнопку, и что форма отправляется
  2. Чтобы сервер не загружался лишними запросами, и чтобы уменьшить вероятность какой-либо ошибки
Понятно, что можно предотвратить лишние нажатия при помощи навешивания специальной визуализации, говорящей, что запрос принят и форма отправляется (простейший пример — показать какой-нибудь анимированный gif). Однако, кнопки-то при этом все равно останутся кликабельными, и самые нетерпеливые пользователи не применут воспользоваться такой возможностью. При этом, на эти дополнительные нажатия форма уже никак не сможет отреагировать (анимированный gif уже крутится), и фрустрация пользователя только увеличится.

Понятно также, что лишние запросы можно предотвратить, повесив на форму какой-нибудь class="form_is_loading", и при всяком сабмите проверять на наличие этого класса. Но зачем делать эти проверки, когда можно обойтись без них, просто сделав кнопку неактивной?

Как делать кнопки неактивными


<input type="submit" onclick="this.disabled=true;">
Этот предложенный в вышеупомянутых топиках простой вариант оказывается недостаточным и неработоспособным.

Почему недостаточно просто делать нажатую кнопку неактивной:
  1. Submit формы может произойти и по нажатию на Enter. Поэтому обработку кнопок надо вешать на событие onsubmit самой формы. К тому же, у формы может быть несколько кнопок, и было бы логичным делать их все неактивными, а не только ту кнопку, которую нажали.
  2. Если после сабмита формы вновь вернуться на страницу с формой (по кнопке «Назад» в браузере), то сработает кеширование: мы столкнемся с неактивными кнопками и не сможем отправить форму еще раз — без принудительной перезагрузки страницы с потерей всех заполненных ранее полей (Возврат к поисковой форме со страницы результатов поиска тут живейший пример).
  3. Если у формы несколько кнопок (например, «Опубликовать» и «Отмена»), то мы не сможем передать серверу, какая именно кнопка была нажата: неактивная кнопка не передает свое имя и значение — даже если мы делаем ее неактивной по событию onsubmit
Итак, сценарий создания неактивных кнопок

Вкратце сценарий таков.
  1. Кнопки делаем неактивными по событию onsubmit формы
  2. Кнопки возвращаем в активное состояние до ухода со страницы по событию window.onunload
  3. Каждая кнопка формы по событию onclick должна создавать одноименное hidden поле, через которое передаст свое значение серверу
А далее следует более развернутый сценарий с макетом кода.

//// html файл //////////////////////////////////////////////////////////////////////

<form id="the_form">
    <input type="submit" name="send" value="Опубликовать">
    <input type="submit" name="cancel" value="Отменить">
</form>

<script>
    formUploader.prepareForm(document.getElementById('the_form'));
</script>

//// js файл ////////////////////////////////////////////////////////////////////////

<script>
formUploader = {

    prepareForm: function(form){

        // Каждая значимая кнопка формы при клике должна создать одноименное hidden поле,
        // чтобы на сервер передалась информация о том, какая кнопка была кликнута
        var allFormFields = form.getElementsByTagName('input');
        for (var i=0; i<allFormFields.length; i++){
            if(allFormFields[i].type == 'submit' && allFormFields[i].name){
                allFormFields[i].onclick = function(){
                    formUploader.createHiddenField(this);
                }
            }
        }

        // Визуализируем форму как отправляемую на сервер на событии onsubmit
        // (в т.ч. делаем все кнопки неактивными)
        form.onsubmit = function(){
            formUploader.setFormLoading(form);
        }

        // Очищаем визуализацию формы (в т.ч. делаем все кнопки вновь активными)
        // при уходе со страницы - по глобальному событию onunload
        window.onunload = function(){
            formUploader.clearFormLoading(form)
        }
    },

    setFormLoading: function(form){
        // Создаем визуализацию загрузки формы и делаем все кнопки неактивными
        // disabled=true;
    },
	
    clearFormLoading: function(form){
        // Очищаем форму от визуализации загрузки и возвращаем кнопки в активное состояние
        // disabled=false;
    },

    createHiddenField: function(button){
        var input = document.createElement('input');
        input.type = 'hidden';
        input.name = button.name;
        input.value = button.value;
        button.parentNode.insertBefore(input, button);
    }
}
</script>
+55
5 января 2009, 08:37
120
karaboz 52,3 G+

комментарии (38)

+1
Liksys #
+1, только добавьте Хаброкат, пожалуйста.
+3
karaboz #
Ага — забыл (= Со стилями оформления заморочился ((= хехе
+1
noita_kronk #
Надеюсь, Хабр не дойдёт до того, что на основе анализа длины топика будет выводить самостоятельно: «НЛО прилетело и предложило сделать хабракат» с соответствующим уведомлением на почту. (-:

Мне кажется достаточно было бы делать это всё на уровне заполнения формы, когда топик ещё пишется.
0
karaboz #
А это НЛО было?? 8-0 ((=
+5
danko #
Ну вот это решение уже можно назвать законченным! :)
+2
den_rad #
Раньше, когда интернет был медленнее, часто встречал проблему: форму отправил, а запрос оборвался. Лучший способ — нажать еще раз на кнопку, а она disabled.
Имхо лучше обрабатывать такие проблемы на стороне сервера.
0
VolCh #
Можно таймер сделать, предотвращающий «дребезг контактов», то есть случайное быстрое нажатие, а то тоже ситуации могут быть, случайно нажал два раза допустим на кнопке «регистрация» а тебе приходит ответ «такой ник уже есть», хотя первую форму сервер обработал нормально и зарегистрировал.
+1
den_rad #
Можно и так сделать. Я бы сделал для каждой формы уникальный ID (например md5(time().rand())) и проверять — отправлялась ли такая форма уже — есть ли такой ID во временной таблице.
0
danko #
Я так пробовал — это то же самое, что дизейблить кнопку. В результате с закэшированной страницы исправленную форму по второму разу не отправишь.
0
den_rad #
Тогда можно модифицировать этот ID на стороне клиента в событии onunload.
0
bat #
А как об этом узнает сервер?
0
den_rad #
Тогда этот код может быть хешем данных. Простым crc32 или чем-то подобным. То что хеш не повторялся — гарантия уникальности данных.
Хотя это очень сложно, отправка формы дважды не такая уж и частая ситуация, чтобы с ней так заморачиваться.
+2
exoundeeq #
uploadForm.setFormLoading(form);

Все ли верно?
0
karaboz #
Ага, спасибо, поправил (=
+6
egorinsk #
Это какой-то бред.

Вам не кажется что вы совсем уж извратились? К тому же ничего не мешает юзеру при тормозах нажать F5 и повторно отправить запрос.

> Если после сабмита формы вновь вернуться на страницу с формой (по кнопке «Назад» в браузере), то сработает кеширование: мы столкнемся с неактивными кнопками и не сможем отправить форму еще раз

Если все сделано правильно (и после обработки действия скрипт редиректит пользователя на GET-страницу) мы вернемся не на предыдущую страницу, а выводжит предупреждение «Вы хотите повторно отправить форму?».

> Кнопки возвращаем в активное состояние до ухода со страницы по событию window.onunload

А в нормальных браузерах работает onunload?
+1
tenshi #
> Вам не кажется что вы совсем уж извратились? К тому же ничего не мешает юзеру при тормозах нажать F5 и повторно отправить запрос.

с аяксом этот фокус не пройдёт.

> Если все сделано правильно…

то мы дадим пользователю возможность отредактировать форму прежде чем отправлять повторно.

> А в нормальных браузерах работает onunload?

да, но не во всех.
+1
karaboz #
> А в нормальных браузерах работает onunload?
Скрипт проверен на Fire Fox, Google Chrome, Opera, Safari, Ie 6, Ie 7
+2
antonhb #
ну ставить disable у кнопок это последнее дело, пользователя вашего сайта это может сильно раздражить, и тогда он может вообще не вернуться на этот сайт. Более понравилось решение с проверкой на стороне сервера. Но это моё мнение, давайте проведем эксперимент по подсчету посетителей до введения такого хака и после)
0
KReal #
Ну, может раздражать, а может и не раздражать. Меня бы больше огорчило, если бы из-за случайного двойного нажатия на кнопку случилась какая-нибудь ошибка.

А как вам, автор, вариант с добавлением на страницу полупрозрачного слоя с индикатором загрузки, подобного тем, что используются во всяких thickbox и тому подобных JS modal dialogs? Проблему, на первый взгляд, решает.
0
karaboz #
Я лично предпочитаю обходить слишком сильную визуализацию. Меня она угнетает (= Но ваш вариант тоже вполне приемлим (= На самом деле, когда мы говорим про disable кнопок, мы скорее все же думаем о том, чтобы сделать для пользователя более очевидными какие-то его действия, чем решаем техническую проблему (=
0
KReal #
Ну да, ну да. В этом плане я согласен скорее с вами, чем с комментатором. То есть, лично у меня никаких вопросов не возникает, когда кнопка дизэйблится. Напротив, приходит понимание, что всё окей)))
+6
timurv #
Еще важно не забывать о CSRF (Cross-Site Request Forgery) атаках:
* isc.sans.org/diary.html?storyid=1750
* en.wikipedia.org/wiki/Cross-site_request_forgery

Решение состоит в том, что:
* при показе формы в сессию и хидден поле кладется ключ
* при отправке формы проверяется ключ из формы с ключом из сессии
* как только получили запрос, из сесии ключ стерли

Пример реализации в Rails api.rubyonrails.org/classes/ActionController/RequestForgeryProtection/ClassMethods.html
+1
GLuKKi #
Спасибо, раньше разрабатывал лишь интуитивно практикуя правило «изменяет только POST».
0
timurv #
Хорошее правило, так и надо делать ;)
+6
roTuKa #
Решение с использованием популярного фреймворка jQuery:

(function($) {
    $.enhanceFormsBehaviour = function() {
        $('form').enhanceBehaviour();
    }

    $.fn.enhanceBehaviour = function() {
        return this.each(function() {
            var submits = $(this).find(':submit');
            submits.click(function() {
                var hidden = document.createElement('input');
                hidden.type = 'hidden';
                hidden.name = this.name;
                hidden.value = this.value;
                this.parentNode.insertBefore(hidden, this)
            });
            $(this).submit(function() {
                submits.attr("disabled", "disabled");
            });         
            $(window).unload(function() {
                submits.removeAttr("disabled");
            })
         }); 
    }
})(jQuery);


Вместо formUploader.prepareForm(document.getElementById('the_form')); нужно написать $.enhanceFormsBehaviour()

Скоро оформлю в виде полноценного плагина, с функциями отклика (callback), на которые можно будет повесить показ различных крутилок-вертелок и иных действий, символизирующих работу формы.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
+1
roTuKa #
Хороший плагин. Универсальный и большой. Предложенный мною код меньше, ориентирован на конкретную проблему/задачу и является просто уменьшенным вариантом описанного в статье кода. Возможно, пригодится кому-нибудь (как минимум, мне самому).
НЛО прилетело и опубликовало эту надпись здесь
+1
roTuKa #
Селектор :submit ловит не только инпуты-сабмиты, но и баттоны-сабмиты.

По событию анлоад я убираю потому, что так сделано в примере кода в статье. Я ж не говорю, что это давно отработанный приём, использующийся на двадцати пяти сотнях сайтов. Я сказал лишь только, что это аналог кода, приведённого в статье.
+1
karaboz #
В публикации дан макет кода. Понятно, что куча тонкостей осталась за кадром, чтобы не загромождать логику.

На AJAX запросах нужно сбрасывать disable при любом ответе с сервера, в т.ч. и при ответе, сигнализирующем ошибку заполнения формы.

При обычном POST мною был найден именно вариант с window.onunload. Попробуйте найти другой метод, было бы интересно (=

Слой тоже можно вешать, но решалась проблема, связанная именно с disable кнопок.
0
karaboz #
Да, и еще если будут подобные вопросы.

В макете кода события задаются как form.onsubmit и window.onunload. Это сделано опять же для легкости чтения. По-правильному надо бы добавлять такие обрабтчики через специальные пользовательские функции, типа addEvent(form, 'submit', myFuntion), чтобы не вступить в конфликт с другими возможными функциями, привязанными к тому же событию…
0
roTuKa #
Кстати, использование jQuery решает эту проблему, потому как у ней внутре неонка внутренняя реализация событий в этом фреймворке как раз использует подобное решение.
0
Nashev #
а о каком именно плагине тут рассказывал НЛО?
+2
Vii #
// при уходе со страницы — по шлобальному событию onunload

забавно, получается ))
0
karaboz #
Хорошо еще, что не получилось по жлобальному (=
p.s. спасибо, поправил
0
psyche #
Не стал читать весь тред. Может уже было выше.
Сценарий таков:
-Пользователь заполняет форму
-Кликает Submit
-Кнопки лочатся
-Пользователь ошибается при заполнении
-На форме отмечаются ошибки ввода
-Кнопки все еще недоступны, нужно поправить поля с ошибками, чтобы кнопка Submit снова стала доступна

Как такой сценарий? По-мойму отличный ограничитель ограничитель бессмысленных кликов и потеряных нервов.
0
karaboz #
Я бы при обнаружении ошибки скорее отлочил бы кнопки. Чтобы тот, кто не понял, что произошла ошибка, кликнул бы по кнопке разок и был бы переброшен по этому действию к полю с ошибкой.

Т.е. идея в том, чтобы делать неактивными кнопки только для сигнала о том, что кнопки были нажаты и еще не завершилось вызванное этим нажатием действие…

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