Отправка форм с помощью Ajax’a

Зачем это нужно?

Каждый раз при отправке формы на сервер, страничка полностью перезагружается, на что уходит трафик и время. Иногда эти потери совсем не оправданы. Представьте, что на страничке с кучей картинок и флэша есть мини-опрос «каким браузером вы пользуетесь?». После того, как пользователь проголосовал, страничка со всем тяжеловесным контентом полностью обновляется. Избежать подобной ситуации можно, если для отправки форм использовать ajax. Эта технология позволяет JavaScript отправлять абсолютно любые http-запросы, в том числе и точно такие же, какие летят на сервер при отправке формы. Прелесть состоит в том, что при выполнении подобного запроса страничка не перезагружается.

Создание объекта XMLHttpRequest

Для выполнения запросов на сервер при помощи JavaScript нам понадобится объект XMLHttpRequest, который, к сожалению, не стандартизирован и создаётся в разных браузерах по-разному. Для кроссбраузерного создания этого объекта напишем функцию:

function xmlHttp() {
  var tran;
  //Для оперы, firefox'a, хрома
  if (window.XMLHttpRequest) {
    tran = new XMLHttpRequest();
  //Для ie
  } else if (window.ActiveXObject) {
    try {
      //Для новых версий
      tran = new ActiveXObject("Msxml2.XMLHTTP");
    } catch (e) {
      //Для cтарых
      tran = new ActiveXObject("Microsoft.XMLHTTP");
    }
  }
  return tran;
}

Отправка запроса

Для отправки GET-запроса будем использовать функцию sendGetRequest:

function sendGetRequest(tran, url, params, callback) {
  if(tran.readyState == 4 || tran.readyState == 0) {    
    tran.open('GET', url + '?' + urlEncodeData(params), true);
    tran.send('');
    if(callback) {
      tran.onreadystatechange = function () {
        if(tran.readyState == 4 && tran.status >= 200 && tran.status < 300)
          callback(tran.responseText);
      }
    }
  }
}

Эта функция, как вы видите, принимает три параметра url, params и callback. Параметр url — это адрес скрипта на сервере, который будет обрабатывать запрос, params — это ассоциативный массив, содержащий параметры запроса. Он может выглядеть, например, вот так:

{login: 'true-coder', mail: 'freeron@ya.ru'}

Последний необязательный параметр callback — это функция, которая будет вызвана, когда сервер ответит на запрос. В качестве параметра она принимает текст ответа. В теле функции sendGetRequest мы первым делом проверяем не занят ли объект tran передачей другого запроса. Свойство readyState содержит число, показывающее на какой стадии находится выполнение запроса. Когда оно равно 0, то запрос ещё не инициализирован, а когда 4, то он уже завершён. В строке

tran.open('GET', url +?+ urlEncodeData(params), true);

мы обращаемся к скрипту с адресом url и определяем тип запроса. Функция urlEncodeData преобразовывает параметры запроса из ассоциативного массива в строку. Её мы рассмотрим позже. Последний параметр функции open, в данном случае равный true, определяет будет выполнен асинхронный или синхронный запрос. В нашем случае запрос асинхронный. При синхронном запросе выполнение сценария приостанавливается до того момента, пока не будет получен ответ. При асинхронном запросе сценарий продолжает исполняться, в то время как выполняется запрос. В строчке

tran.send('');

отправляется сам запрос. Далее проверяется был ли передан при вызове функции параметр callback. Если, был, то присваиваем свойству onreadystatechange функцию, которая будет вызываться каждый раз, когда будет меняться свойство readyState. В теле этой функции будем вызывать функцию callback, но только если запрос завершён (readyState == 4) и при передаче не произошло никаких ошибок (tran.status >= 200 && tran.status < 300). Свойство status содержит статус запроса. Иногда это код ошибки, например, 404. В качестве параметра функции callback передаётся текст ответа сервера.

Функция для отправки POST-запроса аналогична:

function sendPostRequest(tran, url, params, callback) {
  if(tran.readyState == 4 || tran.readyState == 0) {
    tran.open('POST', url, true);
    tran.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    tran.send(urlEncodeData(params));
    if(callback) {
      tran.onreadystatechange = function () {
        if(tran.readyState == 4 && tran.status >= 200 && tran.status < 300)
          callback(tran.responseText);
      }
    }
  }        
}

Только здесь при отправке запроса параметры передаются не вместе с url, а в строчке

tran.send(urlEncodeData(params));

и заголовок запроса Content-Type (тип содержимого запроса), устанавливается равным application/x-www-form-urlencoded.

Предотвращение некорректных запросов и решение проблем с кодировкой

Код функции urlEncodeData, которая позволяет избежать некорректных запросов и помогает устранить проблемы с кодировкой, выглядит вот так:

function urlEncodeData(data) {  
  var arr = [];
  for(var p in data)
    arr.push(encodeURIComponent(p) + '=' + encodeURIComponent(data[p]));
  return arr.join('&');  
}

Эта функция лепит строку вида ‘par1=value1&par2=value2&..&parn=valuen’ из ассоциативного массива {par1: value1, par2: value2, …, parn: valuen}. При этом параметры и их значения обрабатываются функцией encodeURIComponent. Эта функция преобразовывает компоненту запроса к виду, безопасному для передачи на сервер. Она кодируют все символы escape-последовательностями в кодировке UTF-8, кроме букв латинского алфавита, десятичных цифр и !*()’. Например мы хотим передать на сервер следующее данные:

{par1: 'value1&value2', par2: 'value3'}

Тогда тело запроса будет выглядеть вот так: ‘par1=value1&value2&par2=value3’ и на сервере будет принято:

Array ( [par1] => value1, [value2] => '', [par2] => value3 );

Так же эта функция encodeURIComponent позволяет избежать проблем с кодировкой. Дело в том, ajax всегда передаёт текст в UTF-8 и, если этот текст не перекодировать в эту кодировку, то на сервере получим кучу «крокозябр».

Генерация тела запроса

Теперь мы переходим к самому главному — к написанию функции, которую назовём postForm. Она будет формировать и отправлять запрос, основываясь на данных введённых в форму.

function postForm(tran, form, callback) {
  if(form.method == 'POST') {
    sendPostRequest(tran, form.action, getRequestBody(form), callback);
  } else {
    sendGetRequest(tran, form.action, getRequestBody(form), callback);
	}
  return false;
}

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

Функция getRequsetBody принимает в качестве параметра форму. Из данных которые введены в поля формы создаётся ассоциативный массив из пар имяПоля: значенияПоля

function getRequestBody(form) {
  var par = {}, el;
  for(var i = 0; i < form.elements.length; ++i) {
    el = form.elements[i];      
    par[el.name] = el.value;
  }
  return par;
}

Все вышерассмотренные функции сохраним в файл ajax.js.

Пример использования

Теперь проверим, как это всё работает. На запросы будет отвечать скрипт, записанный в файле action.php. Выглядит он вот так:

<?php
header("Content-Type: text/html; charset=Windows-1251");
  if(count($_POST) > 0)
  {
    foreach($_POST as &$v)
      $v = iconv('UTF-8', 'WINDOWS-1251', $v);    
    print_r($_POST);
  }
 
?>

В этом скрипте мы, если в POST-заросе хоть что-то передано, преобразуем значения параметров из кодировки UTF-8, в которой они прилетели в WINDOWS-1251 и отправляем назад на клиент их в форматированном виде. Никакой обёртки данных в html делать не будем, поскольку наша цель всего лишь продемонстрировать работу скрипта.

Саму форму и ещё кое-что разместим в файле index.php. Вот его содержимое:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr" lang="ru-RU">
<head>
<title>ajax post form</title>
<script type="text/javascript" src="ajax.js"></script>
<script type="text/javascript">
  var tran = xmlHttp();
  function callback(data) {
    document.getElementById('answer').innerHTML = data;    
  }
</script>
</head>
<body>  
  <form action="action.php" method="POST" onsubmit="return postForm(tran, this, callback)">
    <span>Ник: </span><input type="text" name="nick" /><br />
    <span>E-mail: </span><input type="text" name="mail" /><br />
    <span>Cообщение: </span><br /><textarea name="text"></textarea><br />
    <input type="submit" value="Отправить" name="submit" />
  </form>
  <div id="answer"></div>
</body>
</html>

В див с id=»answer» будет присылатся ответ сервера. В нашем случае это просто массив $_POST в форматированном виде. Менять содержимое этого дива будет функция

function callback(data) {
  document.getElementById('answer').innerHTML = data;    
}

В обработчике события onsubmit формы вызывается функция postForm. Её код мы рассмотрели ранее. Обратим внимание на то, что эта функция всегда возвращает false, а значит и сам обработчик onsubmit, в нашем случае, возвращает false. Это означает, что форма не отправится стандартным «не аяксовким» способом.

Скачать исходники | Демо

  • Понимаю что нужный механизм, но вот как им пользоваться и куда прописывать, что не догоняю(((

  • Евгений, если хотите воспользоватся этим механихмом, но не хотите писать код сам или не умеете, то можете воспользоваться готовой библиотекой или плагином для jQuery. Их есть великое множество, например, вот этот.

  • Роман

    Большое спасибо за подробное описание, многое пригодилось. Не подскажите как можно доработать функцию getRequestBody в вашем примере, чтобы можно было получить все данные с SELECT MULTIPLE, когда выбранные значения из списка селект передаются в массиве. В данном случае на сервер придёт только первое выбранное значение. Плагины jQuery не хочется использовать.

  • Роман, если я вас правильно понял, то это можно сделать вот так:

    function getRequestBody(form) {
      var par = {}, el;
      for(var i = 0; i < form.elements.length; ++i) {		
        el = form.elements[i];
          if (el.tagName == 'SELECT') {
            addSelectedOptions(el, par);			
          } else {
            par[el.name] = el.value;
          }
      }
      return par;
    }
     
     
    function addSelectedOptions(select, params) {
      for (var i = 0, j = 0; i < select.options.length; i++) {
        if (select.options[i].selected) {
          params[select.name + '[' + (j++) + ']'] = select.options[i].innerHTML;
        }
      }
    }
    header("Content-Type: text/html; charset=Windows-1251");
    if(count($_POST) > 0)
    {
      iconvArr(&$_POST);
      print_r($_POST);
    }
     
    function iconvArr($arr)
    {
      foreach($arr as &$v)
      {
        if (is_array($v))
          iconvArr($v);
        else
          $v = iconv('UTF-8', 'WINDOWS-1251', $v);			
      }
    }

    Исходники и демо обновил.

  • Роман

    Антон, спасибо вам за ответ. Вы меня правильно поняли. Посмотрев ваше решение, убедился что мне ещё учиться и учиться ))) Я изменил эту функцию применительно именно к моей форме, получилось конечно не так универсально и красиво как у вас, будет работать только с моей формой, но с вашего разрешения (для примера) выложу свой вариант:

    function getRequestBody(form) {
    var par = {}, el, a;
    a = 0;
    for(var i = 0; i < 7; ++i) {

    el = form.elements[i];
    par[el.name] = el.value;
    }
    sel = document.getElementById("SelectName1");
    for (var i = 0; i < sel.length; i++) {

    if (sel[i].selected) {
    par[i + 'SelectName1'] = sel[i].value;
    }
    }
    sel2 = document.getElementById("SelectName2");
    for (var i = 0; i < sel2.length; i++) {

    if (sel2[i].selected) {
    par[i + 'SelectName2'] = sel2[i].value;
    }
    }

    for(var i = 9; i < 12; ++i) {
    el = form.elements[i];
    if (el.checked) {
    par[el.name] = el.value;
    }
    }
    return par;
    }

    В данном случае, сначала собираются данные с полей input text, потом с двух селектов, потом с checkbox. Ваш вариант сохранил себе, попробую применить. Еще раз спасибо вам за великолепную статью и ответ 🙂

  • Денис

    Спасибо за скрипт, но работает он почему-то только в опере. в IE9 и в firefox12.0 не работает. не могли ли подсказать, что нужно в нем изменить чтоб скрипт стал кроссбраузерным?

  • Роман

    Денис, у меня данные скрипты работают в IE8, Firefox 11-12, Opera 11.62. В IE9 не проверял.

  • Дима

    Это мать его не работает !!!! Как мне выудить checkbox ?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    function getRequestBody(form) {
      var par = new Array();
      for(var i = 0; i &lt; form.elements.length; ++i) {  
        el = form.elements[i];
    		if (el.type == &#039;checkbox&#039;) {
    			if (el.checked){
    			par[el.name] = &#039;true&#039;;} 
    			else {
    			par[el.name] = &#039;false&#039;;} 
    		}else{
    		if (el.tagName == &#039;SELECT&#039;) {
    			console.log(el);
    			addSelectedOptions(el, par);			
    		} else {
    			par[el.name] = el.value;
    		}
    		}
      }
      return par;
    }
  • Дима

    Простите я не тот файл редактировал. Тот код что я дал работает исправно.

  • Cherry

    Спасибо! А не подскажите, как очистить поля после успешной отправки?

    • Cherry, как-то так

      function clearField(form) {
        for (var i = 0, el; i < form.elemets.length; i++) {
          el = form.elemets[i];
          if (el.tagName == 'INPUT') {
            el.value = '';
          }
        }
      }
    • Спасибо за ответ! Но давайте доведем дело до конца. Я взял ваш пример и потестил на своем сервере. Все понравилось, но убейте, не могу сообразить, как очистить поля формы, даже с вашей подсказкой. После выполнения action.php я буду знать, что данные отправлены и только тогда нужно очистить поля формы, находящейся в index.php.
      Где я должен поместить function
      clearField(form) и ее вызов
      <?php echo "clearField(form);»; ?>
      и как передать форму в качестве параметра? Прошу прощения за бестолковость, но у меня большой опыт в Delphi, а здесь так «ковыряюсь» для души. Думаю многим новичкам будет интересно. Спасибо.

      • Функцию clearField поместите в файл ajax.js, а её вызов внутрь функции callback. Форму можно получить, например, присвоив ей атрибут id, равный my-form, и вызвав метод document.getElementById(‘my-form’).

  • var tran = xmlHttp();
    function callback(data) {
    document.getElementById(‘answer’).innerHTML = data;
    clearField(document.getElementById(‘myform’));
    }

    …………………..

    ……………..
    Ф-ю clearField поместил в конец файла ajax.js
    Поля не очищаются!?

  • var tran = xmlHttp();
    function callback(data) {
    document.getElementById(‘answer’).innerHTML = data;
    clearField(document.getElementById(‘myform’));
    }

    Изменил функцию
    function clearField(form) {
    document.getElementById(‘txtarea’).value=»;
    }
    Теперь поле комментария очищается. Но проблема в том, что очистка поля должна происходить только после успешной отправки формы( после проверки правильности заполнения всех полей).
    В action.php, после успешной проверки, значения полей записываются в базу. И только после этого нужно очистить поле ввода, чтобы после повторного нажатия на кнопку запись в БД не дублировалась. Возможно ли это?

  • Все. Сам сообразил. Анализируем data.
    function callback(data) {
    document.getElementById(‘answer’).innerHTML = data;
    //alert(data);
    if (data==»){
    clearField(document.getElementById(‘myform’));
    }
    Засылаем из action.php в data нужную информацию (в данном случае print «»;), анализируем и очищаем поля или перегружаем страницу.
    Спасибо за внимание!

  • И снова здравствуйте.
    Попробовал запустить демо с вашей страницы под IE8. При отправке формы открывается новая страница. С этим можно бороться?

  • Прошу прощения, но запускаю демо с вашего сайта. Хром — отлично, IE8 — выводит результат на новой странице?