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');