AJAX: реализация трех связанных списков

Пусть у нас есть Интернет-магазин и мы хотим предоставить покупателю удобный интерфейс выбора товара. Для этого мы формируем три выпадающих списка для выбора:

  1. Категории (группы) товаров, например “Мониторы”, “Ноутбуки” и т.п.
  2. Производителя, например, “Samsung”, “Acer” и т.п.
  3. Товара

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

Для начала рассмотрим структуру базы данных нашего магазина.

Таблица categories:

Таблица makers:

Таблица products:

Имея перед глазами эти три таблицы, нетрудно прикинуть, как будут выглядеть наши выпадающие списки:

1=>Мониторы
    1001=>Samsung
        1=>Монитор Samsung 740N
        2=>Монитор Samsung 943N
        3=>Монитор Samsung 2043NW
        4=>Монитор Samsung SM2232BW
    1002=>Acer
        5=>Монитор Acer AL1716FS
        6=>Монитор Acer AL1916CS
        7=>Монитор Acer AL2216WSD
        8=>Монитор Acer AL2416WBSD
    1004=>BenQ
        15=>Монитор BenQ G900W
        16=>Монитор BenQ G700
2=>Ноутбуки
    1001=>Samsung
        11=>Ноутбук Samsung R20
        12=>Ноутбук Samsung R60
    1002=>Acer
        9=>Ноутбук Acer Aspire 5315
        10=>Ноутбук Acer Extensa 5220
    1003=>Toshiba
        13=>Ноутбук Toshiba A210-19A
        14=>Ноутбук Toshiba A210-199

Ну а дальше - реализация нашей идеи:

Файл index.php

<?php
// Соединяемся с сервером базы данных
require 'connect.php';

header("Content-Type: text/html; charset=utf-8");
?>

<html>
<head>
<title>Динамический select</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript" src="ajax.js"> </script>
</head>
<body>
<?php
echo '<form action="'.$_SERVER['PHP_SELF'].'" method="post">'."\n";
// Получаем из БД список категорий
$query = 'SELECT category_id, title FROM categories WHERE 1 ORDER BY category_id';
$res = mysql_query( $query );
echo 'Категории: <select name="category" id="category" onchange="getList(this.value, \'\');">'."\n";
echo '<option value="0">Выберите</option>'."\n";
while ( $ctg = mysql_fetch_array( $res ) ) {
  echo '<option value="'.$ctg['category_id'].'">'.$ctg['title'].'</option>'."\n";
}
echo '</select><br/>'."\n";
?>
Производители:
<select name="maker" id="maker" onchange="getList(this.form.elements['category'].value, this.value);">
<option value="0">Выберите</option>
</select><br/>
Товары: <select name="product" id="product"><option value="0">Выберите</option></select>
</form>
</body>
</html>

Файл getList.php

<?php
// Соединяемся с сервером базы данных
require 'connect.php';

// Если выбрано значение первого списка - формируем второй список
if ( !isset($_GET['maker']) ) {
  // Получаем из БД список производителей
  $query = 'SELECT DISTINCT a.maker_id AS m_id, a.title AS m_title
            FROM makers a INNER JOIN products b
            ON a.maker_id=b.maker_id              
            WHERE b.category_id='
.$_GET['category'].'
            ORDER BY a.maker_id'
;
  $res = mysql_query( $query );
  $makerOptions = '<option value="0">Выберите</option>';
  while ( $mkr = mysql_fetch_array( $res ) ) {
    $makerOptions = $makerOptions.'<option value="'.$mkr['m_id'].'">'.$mkr['m_title'].'</option>';
  }
  $response = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'.
              '<response>'.
                '<action>'.
                'makeMakerList'.
                '</action>'.
                '<options>'.
                $makerOptions.
                '</options>'.
              '</response>';
} else { // Если выбрано значение из списка производителей - формируем список товаров
  $query = 'SELECT product_id, title
            FROM products
            WHERE category_id='
.$_GET['category'].'
            AND maker_id='
.$_GET['maker'].'
            ORDER BY product_id'
;
  $res = mysql_query( $query );
  $productOptions = '<option value="0">Выберите</option>';
  while( $prd = mysql_fetch_array( $res ) ) {
    $productOptions = $productOptions.'<option value="'.$prd['product_id'].'">'.$prd['title'].'</option>';
  }
  $response = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'.
              '<response>'.
                '<action>'.
                'makeProductList'.
                '</action>'.
                '<options>'.
                $productOptions.
                '</options>'.
              '</response>';
}

header('Content-Type: text/xml');
echo $response;
?>

Файл ajax.js

var request = null;
function createRequest() {
  try {
    request = new XMLHttpRequest();
  } catch (trymicrosoft) {
    try {
      request = new ActiveXObject("Msxml2.XMLHTTP");
    } catch (othermicrosoft) {
      try {
        request = new ActiveXObject("Microsoft.XMLHTTP");
      } catch (failed) {
        request = null;
      }
    }
  }
  if (request == null) alert("Ошибка при создании объекта XMLHttpRequest!");
}

function getList(ctg, mkr) {
  document.getElementById("product").innerHTML = '<option value="0">Выберите</option>';
  if ( mkr == "" )
    url = "getList.php?category=" + ctg;
  else
    url = "getList.php?category=" + ctg + "&maker=" + mkr;
  createRequest();
  request.open("GET", url, true);
  request.onreadystatechange = makeList;
  request.send(null);
}
 
function makeList() {
  // только при состоянии "complete"
  if (request.readyState == 4) {
    // для статуса "OK"
    if (request.status == 200) {
      // здесь идут построение списков заново
      responseXml = request.responseXML;
      xmlDoc = responseXml.documentElement;
      action = xmlDoc.getElementsByTagName("action")[0].firstChild.data;
      options = xmlDoc.getElementsByTagName("options")[0].firstChild.data;
      if ( action == "makeMakerList" )
        document.getElementById("maker").innerHTML = options;
      else
        document.getElementById("product").innerHTML = options;
    } else {
      alert("Не удалось получить данные от сервера:\n" + request.statusText);
    }
  }
}

var request = null;
function createRequest() {
  try {
    request = new XMLHttpRequest();
  } catch (trymicrosoft) {
    try {
      request = new ActiveXObject("Msxml2.XMLHTTP");
    } catch (othermicrosoft) {
      try {
        request = new ActiveXObject("Microsoft.XMLHTTP");
      } catch (failed) {
        request = null;
      }
    }
  }
  if (request == null) alert("Ошибка при создании объекта XMLHttpRequest!");
}

function getList(ctg, mkr) {
  var _select = document.getElementById("product");
  _select.innerHTML = ""; // Удаляем всех потомков
  var option = document.createElement("option");
  var optionText = document.createTextNode("Выберите");
  option.appendChild(optionText);
  option.setAttribute("value", "0");
  _select.appendChild(option);
  if ( mkr == "" )
    url = "getList.php?category=" + ctg;
  else
    url = "getList.php?category=" + ctg + "&maker=" + mkr;
  createRequest();
  request.open("GET", url, true);
  request.onreadystatechange = makeList;
  request.send(null);
}
 
function makeList() {
  // только при состоянии "complete"
  if (request.readyState == 4) {
    // для статуса "OK"
    if (request.status == 200) {
      // здесь идет построение списков заново
      var responseXml = request.responseXML;
      var xmlDoc = responseXml.documentElement;
      var action = xmlDoc.getElementsByTagName("action")[0].firstChild.data;
      if ( action == "makeMakerList" ) {
        _select = document.getElementById("maker");        
      } else {
       _select = document.getElementById("product");
      }
      _select.innerHTML = ""; // Удаляем всех потомков
      options = xmlDoc.getElementsByTagName("option");
      for (var i=0; i<options.length; i++) {
        // Извлекаем значение атрибута value и текст
        var value = options[i].getAttribute("value");
        var text = options[i].firstChild.data;
        // Формируем очередной элемент option
        var option = document.createElement("option");
        var optionText = document.createTextNode(text);
        option.appendChild(optionText);
        option.setAttribute("value", value);
        _select.appendChild(option);
      }
    } else {
      alert("Не удалось получить данные от сервера:\n" + request.statusText);
    }
  }
}

Дамп базы данных:

CREATE TABLE `products` (
  `product_id` INT(11) NOT NULL AUTO_INCREMENT,
  `category_id` INT(11) NOT NULL DEFAULT '0',
  `maker_id` INT(11) NOT NULL DEFAULT '0',
  `title` VARCHAR(255) NOT NULL DEFAULT '',
  `price` FLOAT NOT NULL DEFAULT '0',
  PRIMARY KEY  (`product_id`)
) ENGINE=INNODB DEFAULT CHARSET=cp1251;

INSERT INTO `products` VALUES (1, 1, 1001, 'Монитор Samsung 740N', 5700);
INSERT INTO `products` VALUES (2, 1, 1001, 'Монитор Samsung 943N', 6430);
INSERT INTO `products` VALUES (3, 1, 1001, 'Монитор Samsung 2043NW', 7000);
INSERT INTO `products` VALUES (4, 1, 1001, 'Монитор Samsung SM2232BW', 11500);
INSERT INTO `products` VALUES (6, 1, 1002, 'Монитор Acer AL1916CS', 6000);
INSERT INTO `products` VALUES (7, 1, 1002, 'Монитор Acer AL2216WSD', 8900);
INSERT INTO `products` VALUES (8, 1, 1002, 'Монитор Acer AL2416WBSD', 15000);
INSERT INTO `products` VALUES (9, 2, 1002, 'Ноутбук Acer Aspire 5315', 14500);
INSERT INTO `products` VALUES (10, 2, 1002, 'Ноутбук Acer Extensa 5220', 15500);
INSERT INTO `products` VALUES (11, 2, 1001, 'Ноутбук Samsung R20', 15800);
INSERT INTO `products` VALUES (12, 2, 1001, 'Ноутбук Samsung R60', 20100);
INSERT INTO `products` VALUES (13, 2, 1003, 'Ноутбук Toshiba A210-19A', 24700);
INSERT INTO `products` VALUES (14, 2, 1003, 'Ноутбук Toshiba A210-199', 17000);
INSERT INTO `products` VALUES (15, 1, 1004, 'Монитор BenQ G900W', 5000);
INSERT INTO `products` VALUES (16, 1, 1004, 'Монитор BenQ G700', 4800);

CREATE TABLE `categories` (
  `category_id` INT(11) NOT NULL AUTO_INCREMENT,
  `title` VARCHAR(255) NOT NULL DEFAULT '',
  PRIMARY KEY  (`category_id`)
) ENGINE=INNODB DEFAULT CHARSET=cp1251;

INSERT INTO `categories` VALUES (1, 'Мониторы');
INSERT INTO `categories` VALUES (2, 'Ноутбуки');

CREATE TABLE `makers` (
  `maker_id` INT(11) NOT NULL AUTO_INCREMENT,
  `title` VARCHAR(255) NOT NULL DEFAULT '',
  PRIMARY KEY  (`maker_id`)
) ENGINE=INNODB DEFAULT CHARSET=cp1251;

INSERT INTO `makers` VALUES (1001, 'Samsung');
INSERT INTO `makers` VALUES (1002, 'Acer');
INSERT INTO `makers` VALUES (1003, 'Toshiba');
INSERT INTO `makers` VALUES (1004, 'BenQ');

Комментариев: 40

  1. Дмитрий:

    Очень полезное решение для таких как я (не знающих JC и Ajax). Спасибо Вам за это, очень во время. Вот только работает в Firefox, а в IE и Opera нет.
    Подскажите как решить эту проблемку

  2. Роман:

    Спасибо автору. В Ajax вообще не разбираюсь, но многое что подчеркнул для себя.
    Не знаю как в Firefox, но в IE и Opera у меня тоже не работает :(

  3. admin:

    Я исправил ошибку - теперь все работает и в MS IE и в Opera. Как оказалось, эти браузеры не полностью поддерживают innerHTML. Впрочем, поскольку свойство innerHTML не включено в стандарт DOM, винить их в этом трудно. Попытка вставить внутрь тега select элементы option с помощью innerHTML, вызывало ошибку.

  4. Роман:

    Не помогло :( Сделал точную копию таких же таблиц и этот же код, но всеравно ни чё не работает :(

  5. admin:

    Внес изменения - попробуй еще раз: должно работать.

  6. Роман:

    Заработало! :) Огромное спасибо автору за столь прекрасный скрипт и его описание!
    P.S.: Так же огромное спасибо за то что автор не оставляет своих посетителей без своего внимания и быстро реагирует на отзывы :)

  7. Владимир:

    Подскажите, а как передать данные из последнего списка для дальнейшей обработки.

  8. admin:

    Владимир, я не понял вопрос. Куда передать? Что за обработка?

  9. Владимир:

    Вы писали: “Владимир, я не понял вопрос. Куда передать? Что за обработка?”

    Надо внести выбранные категорию, производителя и товар в другую таблицу базы данных.

  10. admin:

    Владимир, так в чем проблема? Надо по событию onchange для третьего списка отправить на сервер с помощью объекта XMLHttpRequest() выбранные значения в первом, втором и третьем списках. Или, если планируется выбрать несколько разных товаров, создать кнопку

    <input type="button" onclick="addToCart();" value="Добавить в корзину" />

    Каждый раз, когда пользователь выбирает очередной товар с помощью выпадающих списков, он может щелкнуть по кнопке “Добавить в корзину”, а мы по этому событию вызываем функцию addToCart(), которая с помощью XMLHttpRequest() отправит запрос на сервер. Серверный скрипт, в свою очередь, выполнит запрос к базе данных на добавление данных в таблицу.

  11. Владимир:

    Спасибо, понял, получилось.

  12. ArtRich:

    Отличный пример! Очень помогло!!!

  13. ArtRich:

    Здравствуйте!
    данный вопрос подымался выше, а именно, необходимо добавить четвертый выпадающий список, зависящий от третьего, подскажите плииз как реализовать это. Дело в том, что я плохо знаком с синтаксисом JAVA. На вид все понятно, а вот на деле никак не получается. Думаю, ни я один столкнулся с этой трудностью.
    Спасибо!

  14. ArtRich:

    Спасибо! Вопрос снят, удалось сделать при помощи копирования функции onchange=”getList … Хотя не совсем правильно, зато работает)

  15. admin:

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

  16. Ленар:

    В опере и мозиле работает, в эксплоере не хочет, в чём может быть причина?

  17. Тимур:

    Здравствуйте! Спасибо за Ваше доброе дело. Я столкнулся с некоторой проблемой. сделал все точно как у Вас. но списки не реагируют друг на друга. Может где-то не подключена поддержка Java? Сборка стандартная=сервер апач, мускл, php..

  18. admin:

    сделал все точно как у Вас. но списки не реагируют друг на друга
    Значит, не все. давайте обсудим это на форуме.

  19. тобик:

    Спасибо, материал помог сориентироваться.

  20. Лео:

    Почему в файлах кодировка utf8 а в БД 1251?

  21. admin:

    Почему в файлах кодировка utf8 а в БД 1251?
    А почему нет?

  22. Виталий:

    В чем может быть причина в ошибке - “Не удалось получить данные от сервера:Not Found”. Дома на локальной машыне все работает.

  23. admin:

    Ошибка “Not Found” - “Не найден (документ, скрипт)”. Скорее всего, неправильно указан путь к серверному скрипту:
    Различие между абсолютными и относительными путями. В файловой системе и на сайте

  24. Виталий:

    Спасибо большое. Оказывается просто напросто пропустил то что в файле getList.php есть большая буква “L”. Вот и мучился пока ненашол что у меня он с маленькой буквой.

  25. TYUS:

    Спасибо большое за пример.
    Из вашего примера сделал себе 4 связанных списка для сервиса автомобильной тематики.

    ЗЫ блог в ридер

  26. retuam:

    Спасибо. Работает. Никак только не могу вывести данные в тертьем селекте в зависимости от первого без выбора значения во втором. Собственно при выборе “Монитор” хотелось бы видеть в третьем селекте все “мониторы” таблицы ‘products’ без ноутбуков?

  27. андрей:

    Спасибо за пример. Очень хороший. Но у меня такой вопрос. Все сделал как у Вас - работает. Начал делать со своей базой работать перестало. Долго искал где ошибка. Но в отличие от Вашего примера у меня идентификаторы (category_id, marker_id и produkt_id) определяются не числами, а словами из латинских символов. Когда я добавил в свою таблицу столбец с идентификатором по id то все пошло. Так что этот скрипт обрабатывает в $_GET['category'] и $_GET['maker'] только числа?

  28. admin:

    этот скрипт обрабатывает в $_GET['category'] и $_GET['maker'] только числа
    Чтобы скрипт работал со строками, надо в SQL-запросе строку заключить в кавычки:
    … WHERE maker_id=’Samsung’

  29. андрей:

    Спасибо большое, теперь все отлично.

  30. Ольга:

    Отличная статья! Все бы хорошо, вот только у меня с кодировкой виимо какая-то проблема, то что берет из базы нечитабельно

  31. Dima:

    Блин гениально! огромное спасибо!

  32. Андрей:

    Ольга:

    Отличная статья! Все бы хорошо, вот только у меня с кодировкой виимо какая-то проблема, то что берет из базы нечитабельно

    Меняете кодировку во всех файлах с UTF-8 на Windows-1251 - все работает.

  33. Евгений:

    Скопировал полностью всё… Списки не реагируют друг на друга.
    Пробовал глобальные переменные присваивать в значения обычных переменных в getList.php, ну и подправлял код. Все равно… списки молчат.

  34. Юрий:

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

  35. Юрий:

    И ещё вопрос, с вашего позволения, а зачем использовать responseXML, почему не просто responseText?

  36. admin:

    И ещё вопрос, с вашего позволения, а зачем использовать responseXML, почему не просто responseText?
    Вам выбирать – text, XML, JSON…

  37. Михаил:

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

  38. Влад:

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

  39. Алексей:

    БОЛЬШОЕ СПАСИБО Автору за приведенный пример!
    Таких как вы не очень много, которые на поставленную задачу приводят для общего доступа её решение. А много тех, кто сидит на форумах и если им такой вопрос задать, они мозг вынесут просто по полной программе и не факт, что человек задавший вопрос, получит на него ответ)
    Еще раз ОГРОМНОЕ вам спасибо! :)

  40. Антон:

    Есть хороший пример динамических списков http://webersoft.ru/select-ajax-mysql/

Оставьте свой отзыв