JQuery-навигация: каскад динамических списков произвольной длины
Добрый день, читатели. Решил сегодня расслабиться от каждодневной работы и написать очередной tutorial. На повестке дня сегодня магия jQuery и небольшой рассказ о том, как динамически создавать на странице группы select`ов. Для того, чтобы вам была понятна задача, которую мы решаем, немного предыстории.
Сейчас я работаю над крупным проектом, который условно можно поделить на две части: 1) парсинг данных со сторонних ресурсов в наше базу данных, 2) расширенный поиск по этой базе с кучей условий и фильтров. Задача, которую мы решим сегодня, появилась именно при создании пользовательского интерфейса для задания условий поиска.
В дело в том, что в информация в базе данных разделена по категориям неограниченной вложенности. И этих самых категорий очень много. Для того, чтобы дать возможность пользователю выбрать категорию для поиска, было решено использовать динамические select`ы. Т.е. изначально на страничку загружается список с категориями верхнего уровня. Если пользователь выбирает для в этом списке какую-то категорию, то мы даем ajax-запрос на специальный скрипт, который возвращает нам список подкатегорий (т.е. тех, для которых выбранная категория является непосредственным родителем).

О том, как организовать подобную иерархию списков, можно прочитать здесь. Но. Такое решение не снимает проблем, ведь оно создано специально под 3 списка, в которых динамически меняется содержимое. А при работе с деревом категорий неограниченной вложенности мы никогда не знаем, сколько списков нам понадобиться. Поэтому описанную в статье методику пришлось “расширить”.
Вооружимся jQuery, PHP и современными традициями AJAX, начнем (сразу скажу, что нам понадобиться jQuery 1.3)…
Для нетерпеливых: то, что у нас получиться в итоге, вы можете посмотреть здесь:
1. Задача
Для того, чтобы не копаться в дебрях сухой теории, придумаем себе конкретную ситуацию. Классические ситуации для использования каскадов списков неограниченной вложенности - каталог товаров в магазине и файловая система. Остановимся на магазине и нарисуем себе, что-нибудь простенькое:
- Компьютерная техника
— Персональные компьютеры
—– Windows
——- Athlon
——- Celeron
——- Pentium
—– Macintosh
—– Прочее
— Ноутбуки
— КПК
— Периферийные устройства- Бытовая техника
— Аудио-аппаратура
— Фотоаппараты
— Микроклиматическое оборудование
—– Вентиляторы
—– Кондиционеры
——- Оконные
——- Навесные
—– Обогреватели- Офисное оборудование
— Копировальная техника
— Факсы
Необходимо при загрузке страницы формировать первый список с родительскими категориями (выделены жирным шрифтом). Далее при выборе элемента в списке, создавать следующий список с подкатегориями. К примеру, пользователь выбирает в списке “Персональные компьютеры”. Мы подгружаем еще один список с содержанием “Windows”, “Macintosh”, “Прочее”. И т.д.
2. Планирование
Заранее продумаем все “тонкости” данной задачи. Списки (select) будем именовать “category_N”, где N - номер “уровня” списка. Первый уровень 0. Следовательно, первый селект на страничке будет иметь атрибут name=”category_0″, следующий name=”category_1″ и т.д.
Список будет заполнен элементами (option), текст которых совпадает с текстом категории, а атрибут value содержит уникальный идентификатор доступа (например, id категории в таблице базы данных).
При выборе любого элемента в любом списке, мы должны:
1) отправить ajax-запрос на серверсайд (пусть это будет ajax/list.php) и передать параметры: value выбранного элемента и уровень (level) создаваемого списка.
2) ajax/list.php возвращет в JSON формате: количество найденных “подкатегорий” и уровень списка, массив “подкатегорий” (если их количество больше 0
).
Далее у нас два варианта:
3) если количество найденных “подкатегорий” 0, то мы должны просто удалить все списки, уровни которых выше, чем уровень списка вызвавшего запрос.
К примеру, у пользователя на страничке были списки: Бытовая техника > Микроклиматическое оборудование > Кондиционеры > Оконные. И он выбирает из второго списка (уровень = 1) Фотоаппараты. У категории Фотоаппараты нет вложенных подкатегорий, поэтому мы должны просто удалить списки с уровнем 2 и 3.
4) если количество найденных подкатегорий больше 0, то мы ищем список с нужным уровнем. Если таковой существует, очищаем его и заполняем нужными данными. Все старшие удаляем. Если списка такого уровня нет, то мы создаем его с нужным набором option`ов.
Также не забываем, что пользователи особи нетерпеливые, а ajax запрос требует некоторое время. Показывать картинки для сигнализации о том, что запрос обрабатывается, конечно, можно. Но это, мягко говоря, опасно. Если пользователь не дождется выполнения запроса и выберет еще один пункт меню, то потом может случиться непредвиденное
Поэтому при отправке ajax-запрос заблокируем все списки из нашего каскада. При получении ответа, соответственно эту блокировку снимем.
5) Для повышения юзабильности наших списков, вызывать ajax - запрос будем только в том случае, если пользователь сменил текущую позицию списка. Т.е. если он кликнул по списку, прокрутил его…, посмотрел.., подумал… и снова кликнул на тот пункт, который уже был выбран - то делать запрос снова, переопределять следующий список нам не смысла. Добиться этого можно используя событие onChange для элемента select.
3. PHP serverside
В исходном проекте у меня была база данных, но сейчас мы не будем так все усложнять. Сделаем два массива - один с названиями категорий, другой с идентификатором родительской категории. Такой объем данных позволяет нам это сделать
Напомню, что серверсайд принимает два параметра POST-запроса: pcategory и level. Из найденных данных формируем JSON и отдаем клиенту.
/** * @project: SelectTutorial * @file: ajax/list.php * @author Alex Kachayev <kachayev@gmail.com> * http://www.kachayev.ru * @version: 1.0.0000 * * Returns categories list from array * This is only example for learning * jQuery and Ajax */ /* Если это не AJAX.. умираем :) */ if( $_SERVER['HTTP_X_REQUESTED_WITH'] != 'XMLHttpRequest') die( 'Request Error!' ); /** * @var array Здесь содержаться названия категорий: * [идентификатор категории] => 'название' * @todo Это только учебный пример, в реальной * ситуации вы скорее всего воспользуетесь * для этого базой данных или XML файлом */ $categories = array( 'x0' => 'Компьютерная техника', 'x1' => 'Персональные компьютеры', 'x2' => 'Windows', 'x3' => 'Athlon', 'x4' => 'Celeron', 'x5' => 'Pentium', 'x6' => 'Macintosh', 'x7' => 'Прочее', 'x8' => 'Ноутбуки', 'x9' => 'КПК', 'x10' => 'Периферийные устройства', 's0' => 'Бытовая техника', 's1' => 'Аудио-аппаратура', 's2' => 'Фотоаппараты', 's3' => 'Микроклиматическое оборудование', 's4' => 'Вентиляторы', 's5' => 'Кондиционеры', 's6' => 'Оконные', 's7' => 'Навесные', 's8' => 'Обогреватели', 'k0' => 'Офисное оборудование', 'k1' => 'Копировальная техника', 'k2' => 'Факсы' ); /** * @var array В этом массиве мы храним идентификаторы * родительских категорий. '0' означает, что * категория не имеет родительской * @see $categories */ $levels = array( 'x0' => '0', 'x1' => 'x0', 'x2' => 'x1', 'x3' => 'x2', 'x4' => 'x2', 'x5' => 'x2', 'x6' => 'x1', 'x7' => 'x1', 'x8' => 'x0', 'x9' => 'x0', 'x10' => 'x0', 's0' => '0', 's1' => 's0', 's2' => 's0', 's3' => 's0', 's4' => 's3', 's5' => 's3', 's6' => 's5', 's7' => 's5', 's8' => 's0', 'k0' => '0', 'k1' => 'k0', 'k2' => 'k0' ); /** * Получаем значение параметров из запроса * либо устанавливаем значения по умолчанию */ $pcategory = isset($_POST['pcategory']) ? $_POST['pcategory'] : 0; $level = isset($_POST['level']) ? $_POST['level'] : 0; /** * @var array Результирующий список категорий * [item] => Массив категорий * [id] => Идентификатор * [name] => Название * [count] => Количество категорий * [level] => Уровень каскада */ $list = array(); /** * @var array Промежуточный массив идентификаторов категорий */ $result = array(); /** * Получаем идентификаторы нужных нам категорий * @see http://www.php.net/array_keys */ foreach( array_keys($levels, $pcategory) as $value) $result[] = array('id'=>$value, 'name'=>$categories[$value]); /* Заносим категории в результирующий массив */ if(count($result)) $list['item'] = $result; /* Указываем дополнительные параметры */ $list['count'] = count($result); $list['level'] = $level; /* Явно указываем формат данных */ header('Content-type: application/json'); /** * Преобразуем в массив в JSON * @see http://www.php.net/json_encode */ echo json_encode($list);
Обратите внимание, мы использовали функцию json_encode, которая преобразует переданные ей значения в JSON формат.
4. Расширяем возможности jQuery
Для своего же удобства создам три дополнительных jQuery-метода. Идею такого подхода, я вычитал у Геннадия, ссылку на статью которого я приводил выше. Спс ему большое
Итак, нам понадобиться следующие методы: очищение списка, заполнения списка необходимыми данными и удаление элементов по заданному селектору, следующих за нужным нам элементом. Первые два понятно зачем, а третий мы будем использовать для того, чтобы удалять списки старшего уровня.
Код откомментирован, думаю это поможет разобраться:
(function($){ /* Очищаем select */ $.fn.clearSelect = function() { return this.each(function(){ /* Проверяем является ли элемент select`ом */ if(this.tagName=='SELECT') { this.options.length = 0; /* Блокируем на время заполнения */ $(this).attr('disabled','disabled'); } }); } /* Удаляем старшие элементы */ $.fn.clearField = function(selector) { /** * Ищем все элементы следующие за вызывавшим * и удовлеторяющие переданному селектору */ this.nextAll(selector).remove(); return this; } /* Заполняем select переданными данными */ $.fn.fillSelect = function(dataArray) { return this.clearSelect().each(function(){ /* Проверяем является ли элемент select`ом */ if(this.tagName=='SELECT') { var currentSelect = this; /* Устанавливаем этот option первым в списке */ if($.support.cssFloat) { currentSelect.add(start,null); } else { currentSelect.add(start); } $.each(dataArray,function(index,data){ /* Если определено 'name' */ if(data.name) { /* Создаем новый option */ var option = new Option(data.name,data.id); /* Добавляем новый option к select`у */ if($.support.cssFloat) { currentSelect.add(option,null); } else { currentSelect.add(option); } } }); /* Выделяем первый элемент списка */ $(this).removeAttr('disabled').find('option:first').attr('selected', 'selected'); } }); } })(jQuery);
5. Получение списка категорий
Теперь напишем JavaScript-функцию getCategory(), которая будет инициализировать ajax-запрос. Основные моменты ее работы мы уже обговорили, для остального есть комментарии и в коде.
/* Функция отсылает ajax-запрос */ function getCategory(pcategory, level) { $.ajax({ url: 'ajax/list.php', type: 'POST', data: 'pcategory='+ pcategory +'&level='+ level, dataType: 'JSON', timeout: 5000, beforeSend: function(){ // Блокируем все необходимы select`ы $('select[name^=category_]').attr('disabled', 'disabled'); }, complete: function(){ // Снимаем блокировку $('select[name^=category_]').removeAttr('disabled'); }, success: function(response){ var data = eval('('+ response +')'); // Если количество категорий в ответе 0 либо не определено if(data.count === 'undefined' || data.count == 0) { // просто удаляет старшие уровни каскада $('select[name=category_'+ (data.level - 1) +']') .clearField('select[name^=category]') .clearField('span'); return false; } if( $('select[name=category_'+ data.level +']').length ) { // Если select этого уровня уже существует // мы должны удалить все старшие select`ы, // очистить старые данные и заполнить новым контентом $('select[name=category_'+ data.level +']') .clearField('select[name^=category]') .clearField('span') .fillSelect(data.item); } else { // Если select этого уровня не существует, // мы должны его создать и заполнить данными $('#categories select:last').after('<span>></span> <select name="category_'+ data.level +'"></select>'); $('select[name=category_'+ data.level +']').fillSelect(data.item); } /* Сбрасываем старый обработчик */ $('select[name=category_'+ data.level +']').unbind('change'); /* Вешаем новый */ $('select[name=category_'+ data.level +']').change(function(){ return clickEvent($(this)); }); return false; }, error: function(){ // Сообщаем пользователю, что произошла ошибка $('#msg').append('<p>Some error with categories. Please, try later ;)</p>'); return false; } }); }
6. Обработчик события
Давайте теперь определим ту самую функцию-обработчик, которую уже подцепили на наши списки в предыдущем пункте.
function clickEvent(select) { var id = select.find('option:selected').attr('value'); /** * Если id=-1, значит выбран пункт "Выбор.." * значит мы должны просто очистить старшие списки */ if (id == '-1') { select.clearField('select[name^=category]').clearField('span'); return false; } var level = parseInt(select.attr('name').replace('category_', '')) + 1; return getCategory(id, level); }
Как только DOM готов к обработке, вызываем getCategory() для получения первого списка.
$(document).ready(function(){ /* Получаем список категорий */ getCategory(0, 0); });
7. Что дальше?
Естественно, все вышеописанное - только “каркас”, на который можно еще очень много чего по прикручивать. Так что, дерзайте!
P.S. Код будет работать только под jQuery 1.3 Вообще, разницы мелочь… но все же. В предыдущих версиях jQuery мы бы писали select[@name=], а не select[name=], как делаем это сейчас.
Понравился пост? Будь в курсе последних событий: подпишись на RSS-ленту.!
Также читайте по теме:
- 8 февраля

Афигеть, вот это супер, я мечтал такое простое реализовать, но моих познаний JS не хватало. Огромное спасибо вам за это. Я очень вам благодарен:)
упс, смотрел на Mozilla Firefox 3.0.6 работает на отлично а вот попробовал на IE 6.0.2900.2180 при выборе первого списка другие не подгружаются
2Андрей:
Сейчас гляну. Когда проверял, не обратил внимание.
Исправил
Все дело в том, что я использовал событие “change”. Но при создании списка мы автоматически выделяет первый его элемент. Вот в этой строке:
$(this).removeAttr(’disabled’).find(’option:first’).attr(’selected’, ’selected’);
Соответственно, при клике по первому элементу, браузер не генерирует события “change”. Я решил эту проблемку путем использования события “click” для option`ов. Правда теперь можно подумать о какой-нибудь функции кеширования… но это уже позже.
Спасибо за замечание
спасибо за оперативность, сейчас проверил, странно , но список по прежнему не срабатывает((
При выборе какого либо элемента, ничего не происходит, ни ошибок ничего не выдаёт, страницу обновлял кнопкой F5. А в ФФ всё отлично.
версия WinXP и IE у меня
6.0.2900.2180.xpsp_sp2_rtm.040803-2158
Довольно интересный подход к проблеме. Спасибо за советы.
Можно попробовать вместо live() использовать непосредственно click(), IE6 очень строго к подобным функциям относится.
Динамические селект списки вообще довольно распростроненная задача, поэтому статья полезная. Мне как-то довелось реализовывать связанные автокомплит поля на манер селет списков - дикое количество скрытых обработчиков на странице. Жаль в то время не попался туториал на тему, очень пригодился бы.
У меня, кстати, тоже не срабатывает, Алексей, Вы ответите?
2Павел:
Да, Павел. Отвечу
Сегодня попробую заменить live() либо на функцию, которая будет реализовать bind() для каждого новосозданного списка. Либо с помощью плагина к jQuery. Как только получиться позитивный результат, обязательно напишу.
P.S. Я вообще IE обажаю
Спасибо Алексей, буду надеяться что всё у вас получится.
Только что попробовал, и ни чего не вышло, жду Алексей решения проблемы.
“Я решил эту проблемку путем использования события “click” для option`ов”
OMG, это совсем плохо.
1) Я совсем не сразу понял что нужно еще раз выбрать уже выбраный пункт в select-е. А я всего-лиш user
2) Если я буду использовать клавиатуру то ничего не работает.
Как по мне то лучше сделать фиктивный пунк меню “Выбрать” и вернуть “change” обратно. Для пользователя намного проще.
Второе:
А как же принцип unobtrusive JavaScript? Без JavaScript совсем работать не будет, это плохо.
А так спасибо за статью. Интерсная.
Да ладно, во многих приложениях используют click даже поверх onchange. Люди не жалуются
на счет js-unobtrusive, то знаете ли, этот принцип сложно реализовать в js-коде))))))))) Это реализуется в совокупности php+css+html уже кому как надо - под конкретный проект, автор же, как я понял, предлагает разработку касающуюся исключительно js
кстати спасибо, автор, здорово и просто :):):)
@riddi
“Да ладно, во многих приложениях используют click даже поверх onchange. Люди не жалуются”
Я жалуюсь :). И вообще это очень профессионально говорить “да ладно”, “ничье, и так пойдет”, “да какая разница” :).
Относительно “предлагает разработку касающуюся исключительно js” согласен. Но статья не полная, если не упомянуть, что для того, чтобы приложение работало, как говорится, bullet-proof нужно сделать non-javascript версию.
@Алексей Качаев
Относительно “live() либо на функцию, которая будет реализовать bind() для каждого новосозданного списка”
Есть такая техника - event delegation, для jQuery есть пример на http://www.danwebb.net/2008/2/8/event-delegation-made-easy-in-jquery.
live() делает то же, но на сайте написано, что пока не работает с change. http://docs.jquery.com/Events/live
Не уверен, что сработает delegation, думаю, если в 1.3 не работает live(), то сомнительно, чтобы что-то другое, старше, заработало, но можно попробовать
Новая версия каскада готова.
Я лично проверил на Firefox 3.0.6, IE 7, Google Chorme 1.0.154, Opera 9.5. Кто протестирует на более старых версиях браузеров - буду признателен
Сделал с помощью динамического “навешивания” change(). Для удобства, по совету Eater добавил фиктивный пункт “Выбрать”. Который, кстати, удобен, если пользователь сначала раскрыл старшие селекты, а потом вдруг передумал - выбором этого пункта он удаляет старший каскад.
По поводу unobtrusive JavaScript, то к сожалению, разрабатывать js код на этих принципах пока не умею. Если у вас получиться реализовать что-то подобное в стиле unobtrusive JS - с удовольствием почитаю
класно, теперь всё работает, спасибо огромное:)
проверял через
Firefox/3.0.6 и IE 6.0 работает отлично
а у меня все равно ничего не работает, правда проверял через лису исключительно
2Яков:
Укажите, пжл, точную версию вашего браузера. В лисе то, с самого начало все работало…
[...] вроде никогда о нем не писал). …И даже о том, что мой каскад динамических списков не работает в Opera 12 (!!!). Неужели я один так отстал от [...]
[...] JQuery-навигация: каскад динамических списков произвольн… [...]
Не работает в IE 5.5 и соответственно ниже
Смотрите официальные доки jQuery. Там ясно сказано - корректная работа с IE 6.0+, FF 2+, Opera 9.0+, Safari 3.0 +.
Для начинающих выложили бы работающий архив как на примере, который выше сами указали http://www.linkexchanger.su/2009/82.html , а то не понятно какой фрагмент программы в какой вайл записывать. На уже работающем примере легче разобраться и поня что к чему.
2selex:
Имхо, разбираясь с этим вопросом сами - куда быстрее получите нужные навыки.
Алексей, скажи пожалуйста как сделать так, чтоб вместо последнего select`а появлялась форма типа checkbox, для случая возможности выбора нескольких категорий?
Автору огромное спасибо. Хотелось бы спросить про функцию eval(), для чего она необходима? У меня без нее ничего не получилось. Точнее мой вариант несколько отличается от всего выше представленного.
Здравсвуйте!
Хочу попробовать запустить этот же пример, но кроме пункта “Выбрать…” в первый селект ничего больше не подгружается((
Вроде все сделала правильно.. и jQuery есть, и php файл, и сами скрипты…
Подскажите, в чем может быть причина?
событие change не генерируется
Функция очистки списка работать правильно только в том случае если список не содержит группировки пунктов .
Иначе строку
this.options.length = 0;
надо менять на что-то вроде:
while (this.childNodes.length) {
if (this.firstChild.tagName == ‘OPTGROUP’) {
while (this.firstChild.childNodes.length) {
this.firstChild.removeChild(this.firstChild.firstChild);
}
}
this.removeChild(this.firstChild);
Ребятки, автор упустил маленькую деталь… Это проблема с кодировкой в функции php “json_encode”. У вас просто ничего не будет показываться , кроме первого селекта. Решение есть в сети. Вижу, что многие с этим мучаются, а простые и важные вопросы не были опубликованы. Приятно, что автор уделил время поделиться, но прошу прощения, полуфабрикаты мало кто делает, это плохой тон в программировании. Не вижу еще и кода html в открытом виде, пришлось слизывать с примера на сайте, чтобы проверить, все ли работает… Так что даже опытный пользователь не сразу поймет, почему код не работает. Если бы я не загрузил Firebug и не отследил переменные, я бы еще долго мучался, хотя не первый месяц “за рулем”.
А как отобразить выбранные данные в селектах после пост запроса?
Народ подскажите что из себя представляет форма:
я ее такой нарисовал(или списал)
Категория:
У мене теж не працює. я провіряв в усіх бровзерах. Викладіть архів!
В IE синтаксическая ошибка ругается на эту строку
var data = eval(’('+ response +’)');
форму написал такую это правильно:
Категория:
В список ничего не загружается помогите плиз???
форма такая:
[form name="search" action="" method="post"]
[table width="973" bgcolor="#f2f2f2" border="0" cellpadding="5" cellspacing="5"]
[tbody]
[tr]
[td]
[div align="right">Категория:[/div]
[/td]
[td id="categories"]
[select name=""]
[/select]
[/td]
[/tr]
[/tbody]
[/table]
[/form]
Спасибо Алексею, пример работает. но с кодировкой проблема.
база данных mysql кодировка utf8 general ci
Извеняюсь за флуд не знаю как убрать верхние надписи но проблема у меня в том что пришлось закоментировать //header(’Content-type: application/json;’); и тогда стало работать но опять с ошибкой :
- поддерживается только английский шрифт, надписи на русском не отображаются;
- первый селект у меня пустой а все начинает работать со второго, то есть во втором селекте уменя данные которые должны быть в первом
спасибо помогите!
вот решение проблемы для русского языка
function json_safe_encode($var)
{
return json_encode(json_fix_cyr($var));
}
function json_fix_cyr($var)
{
if (is_array($var)) {
$new = array();
foreach ($var as $k => $v) {
$new[json_fix_cyr($k)] = json_fix_cyr($v);
}
$var = $new;
} elseif (is_object($var)) {
$vars = get_object_vars($var);
foreach ($vars as $m => $v) {
$var->$m = json_fix_cyr($v);
}
} elseif (is_string($var)) {
$var = iconv(’cp1251′, ‘utf-8′, $var);
}
return $var;
}
echo json_safe_encode($list);
Остается вторая проблема:
первый селект у меня пустой а все начинает работать со второго, то есть во втором селекте уменя данные которые должны быть в первом
спасибо помогите!
Справа в самому файлі php - якщо всі російські букви на латиницю, то воно запрацює!!!