Zależne od siebie select listy
JavaScript, PHP, SQL 8 grudnia 2009Swego czasu gdy dopiero zaczynałem swoją przygodę z programowaniem web’owym natrafiłem na pewien mur nie do przeskoczenia, było nim zrobienie 2 zależnych od siebie select list których dane były by pobierane z bazy danych. Trudziłem się z tym niemiłosiernie aż w końcu odpuściłem i zrobiłem to jakimś mykiem. Teraz gdy poznałem już trochę zasady działania pewnych funkcji, poznałem zasady działania skryptów zmierzyłem się z owym problemem który obecnie wydaje się banalne prosty. Pisze ten artykuł dla osób „nowych” którzy tak jak i ja mają problem z zależnymi select’ami.
1. Czym się to je – czyli jak zacząć?
Aby stworzyć zależne od siebie selecty musimy pokazać że do takiego działania potrzebna jest, jednym bardziej drugim mniej, znana technologia jaką jest AJAX. Cóż można o nim powiedzieć, jak już wspomniałem jest to technologia oparta o JS, ale myślę że nie ma sensu powielać tego co można znaleźć na Wikipedii więc zainteresowanych odsyłam właśnie tam: Wikipedia: AJAX. W naszym przykładzie wykorzystam „czystego” AJAX‘a – czystego w sensie beż żadnej domieszki różnych dziwnych frameworków typu jQuery (selecty oparte właśnie o ten frameworka może kiedy indziej
).
2. Wygląd tabel w bazie danych
Będę pokazywał na przykładzie żywo wyjętym z mojej bazy więc zacznijmy od pokazania struktury tabel na postawie których będziemy wyświetlać select listy.
Tabela: list1
| MySQL | | copy code | | ? |
| 1 | CREATE TABLE IF NOT EXISTS `list1` ( |
| 2 | `id` int(11) NOT NULL AUTO_INCREMENT, |
| 3 | `nazwa` varchar(100) CHARACTER SET utf8 COLLATE utf8_polish_ci NOT NULL, |
| 4 | PRIMARY KEY (`id`) |
| 5 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8_polish_ci AUTO_INCREMENT=1; |
Dane w tabeli wyglądają tak:
| MySQL | | copy code | | ? |
mysql> SELECT * FROM list1; |
+----+---------+ |
| id | nazwa | |
+----+---------+ |
| 1 | wybór_1 | |
| 2 | wybór_2 | |
| 3 | wybór_3 | |
| 4 | wybór_4 | |
+----+---------+ |
Teraz tabela: list2
| MySQL | | copy code | | ? |
| 1 | CREATE TABLE IF NOT EXISTS `list2` ( |
| 2 | `id` int(11) NOT NULL AUTO_INCREMENT, |
| 3 | `nazwa_list2` varchar(100) CHARACTER SET utf8 COLLATE utf8_polish_ci NOT NULL, |
| 4 | `id_list1` int(11) NOT NULL, |
| 5 | PRIMARY KEY (`id`) |
| 6 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8_polish_ci AUTO_INCREMENT=1 ; |
no i dane z tej tabeli:
| MySQL | | copy code | | ? |
mysql> SELECT * FROM list2; |
+----+-------------+----------+ |
| id | nazwa_list2 | id_list1 | |
+----+-------------+----------+ |
| 1 | wybór_1_1 | 1 | |
| 2 | wybór_1_2 | 1 | |
| 3 | wybór_1_3 | 1 | |
| 4 | wybór_2_1 | 2 | |
| 5 | wybór_2_2 | 2 | |
| 6 | wybór_3_1 | 3 | |
| 7 | wybór_3_2 | 3 | |
| 8 | wybór_3_3 | 3 | |
| 9 | wybór_4_1 | 4 | |
+----+-------------+----------+ |
Teraz małe objaśnienie, w tabeli list1 mamy zadeklarowane możliwości wyboru które będą pojawiać się w pierwszej select liście. Natomiast tabela 2 pokazuje nam odpowiednie pod punkty które będą wyciągane za pomocą pola id_list1.
Teraz można powiedzieć „serce” naszego skryptu, jest to część kodu która odpowiada za całe wyświetlanie.
| Javascript | | copy code | | ? |
| 01 | <script type="text/javascript"> |
| 02 | var xmlhttp; |
| 03 | |
| 04 | function showList(str) |
| 05 | { |
| 06 | xmlhttp=GetXmlHttpObject(); |
| 07 | var url="pokaz_listy.php"; |
| 08 | url=url+"?q="+str; |
| 09 | xmlhttp.onreadystatechange=stateListChanged; |
| 10 | xmlhttp.open("GET",url,true); |
| 11 | xmlhttp.send(null); |
| 12 | } |
| 13 | |
| 14 | function stateListChanged() |
| 15 | { |
| 16 | if (xmlhttp.readyState==4) |
| 17 | { |
| 18 | document.getElementById("pokaz").innerHTML=xmlhttp.responseText; |
| 19 | } |
| 20 | } |
| 21 | function GetXmlHttpObject() |
| 22 | { |
| 23 | if (window.XMLHttpRequest) |
| 24 | { |
| 25 | return new XMLHttpRequest(); |
| 26 | } |
| 27 | if (window.ActiveXObject) |
| 28 | { |
| 29 | return new ActiveXObject("Microsoft.XMLHTTP"); |
| 30 | } |
| 31 | return null; |
| 32 | } |
| 33 | </script> |
Myślę że nie ma sensu opisywać każdej funkcji osobno ponieważ od tego są różnej maści manuale. Opiszę działanie poszczególnych funkcji. Funkcja showList odpowiedzialna jest za odpalenie pliku pokaz_listy.php, przesyłając do niego tablice super globalną $_GET, natomiast funkcja stateListChanged, pokazuje nam w odpowiednim miejscu o co chcemy zobaczyć czyli naszego drugiego selecta, otrzymanego względem wyboru w pierwszym.
Kolejnym krokiem jest stworzenie selecta na podstawie którego będziemy wyświetlać dane w innej select liście.
| PHP | | copy code | | ? |
| 01 | include ('mysql.php'); |
| 02 | |
| 03 | $list1_query = mysql_query('SELECT * FROM list1'); |
| 04 | |
| 05 | echo '<select name="list1" onchange="showList(this.value)">'; |
| 06 | while($row_list1 = mysql_fetch_array($list1_query)) |
| 07 | { |
| 08 | echo '<option value="'.$row_list1['id'].'">'.$row_list1['nazwa'].'</option>'; |
| 09 | } |
| 10 | echo '</select>'; |
| 11 | <div id="pokaz"></div> |
W tym skrypcie kolejno łączymy się z bazą, wyświetlamy selcta i w div’ie pokaz pokazujemy pożądaną select listę.
Bardzo ważną rzeczą, powiem wręcz kluczową jest ustawienie zdarzenie w JS onchange. Bez poprawnego wywołanie tego zdarzenia nie będziemy w stanie nic zobaczyć
I przyszedł teraz czas na plik pokaz_liste.php, to w nim odbywa się cały proces tworzenia listy i wyświetlania jej na stronie.
| PHP | | copy code | | ? |
| 01 | if($_GET['q']) |
| 02 | { |
| 03 | include ('mysql.inc'); |
| 04 | $q=mysql_escape_string($_GET["q"]); |
| 05 | |
| 06 | $list2_query= mysql_query('SELECT * FROM list2 WHERE id_list1 = '.$q.''); |
| 07 | |
| 08 | echo '<div id="pokaz"><select name="list1" onchange=(this.value)>'; |
| 09 | while($row_list2 = mysql_fetch_array($list2_query)) |
| 10 | { |
| 11 | echo '<option value="'.$row_list2['id'].'">'.iconv("ISO-8859-2","UTF-8", $row_list2['nazwa_list2']).'</option>'; |
| 12 | } |
| 13 | echo '</select></div>'; |
| 14 | } |
Zmienna $q jest otrzymywana poprzez właśnie nasz ajaxowy skrypt. I właśnie na jej podstawie zostają podane warunki do klauzuli WHERE w zapytaniu SQL.
Tym sposobem dobrnęliśmy do końca tego artykułu. Jak to mówią strach ma wielkie oczy, tak właśnie w tym przypadku odbyło się bez jakichkolwiek egzorcyzmów, zaklinania deszczu czy czego tam jeszcze.
No i na koniec jeszcze listingi całości:
Plik index.php
| PHP | | copy code | | ? |
| 01 | <script type="text/javascript"> |
| 02 | var xmlhttp; |
| 03 | |
| 04 | function showList(str) |
| 05 | { |
| 06 | xmlhttp=GetXmlHttpObject(); |
| 07 | |
| 08 | var url="pokaz_liste.php"; |
| 09 | url=url+"?q="+str; |
| 10 | xmlhttp.onreadystatechange=stateListChanged; |
| 11 | xmlhttp.open("GET",url,true); |
| 12 | xmlhttp.send(null); |
| 13 | } |
| 14 | |
| 15 | function stateListChanged() |
| 16 | { |
| 17 | if (xmlhttp.readyState==4) |
| 18 | { |
| 19 | document.getElementById("pokaz").innerHTML=xmlhttp.responseText; |
| 20 | } |
| 21 | } |
| 22 | function GetXmlHttpObject() |
| 23 | { |
| 24 | if (window.XMLHttpRequest) |
| 25 | { |
| 26 | return new XMLHttpRequest(); |
| 27 | } |
| 28 | if (window.ActiveXObject) |
| 29 | { |
| 30 | return new ActiveXObject("Microsoft.XMLHTTP"); |
| 31 | } |
| 32 | return null; |
| 33 | } |
| 34 | </script> |
| 35 | |
| 36 | <? |
| 37 | include ('mysql.php'); |
| 38 | |
| 39 | $list1_query = mysql_query('SELECT * FROM list1'); |
| 40 | |
| 41 | echo '<select name="list1" onchange="showList(this.value)">'; |
| 42 | while($row_list1 = mysql_fetch_array($list1_query)) |
| 43 | { |
| 44 | echo '<option value="'.$row_list1['id'].'">'.$row_list1['nazwa'].'</option>'; |
| 45 | } |
| 46 | echo '</select>'; |
| 47 | ?> |
| 48 | <div id="pokaz"></div> |
Plik: pokaz_liste.php
| PHP | | copy code | | ? |
| 01 | if($_GET['q']) |
| 02 | { |
| 03 | include ('mysql.inc'); |
| 04 | $q=mysql_escape_string($_GET["q"]); |
| 05 | |
| 06 | $list2_query= mysql_query('SELECT * FROM list2 WHERE id_list1 = '.$q.''); |
| 07 | |
| 08 | echo '<select name="list1" onchange=(this.value)>'; |
| 09 | while($row_list2 = mysql_fetch_array($list2_query)) |
| 10 | { |
| 11 | echo '<option value="'.$row_list2['id'].'">'.iconv("ISO-8859-2","UTF-8", $row_list2['nazwa_list2']).'</option>'; |
| 12 | } |
| 13 | echo '</select>'; |
| 14 | } |
EDIT
Chociaż jednym słowem wypadałoby coś powiedzieć o filtrowaniu tablic $_GET.
Zgodnie z sugestią dodałem filtrowanie tablicy $_GET za pomocą funkcji mysql_escape_string.
8 grudnia 2009 at 22:30
Nie zawsze wykorzystanie ajax jest dobrym rozwiązaniem.
IMHO wystarczyłoby załadować wszystkie selecty do tablicy i za pomocą JS zmieniać zawartość selecta zależnego.
9 grudnia 2009 at 08:57
tylko takie rozwiązanie wydaje się trochę mało wydajne, ale mogę się mylić bo nigdy czegoś takiego nie stosowałem.
9 grudnia 2009 at 10:24
” $q=$_GET["q"]; ”
Chociaż jednym słowem wypadałoby coś powiedzieć o filtrowaniu tablic $_GET.
SebaZ ma racje ponieważ wtedy wykonuje się jedno zapytanie do bazy i reszta zajmuje sie JS.
W tym przypadku zapytanie do bazy jest wykonywane za każdym razem podczas zdarzenia „onchange()”. Oczywiście przy „mini-serwisach” nie ma to większego znaczenia :]
9 grudnia 2009 at 10:35
fakt o filtrowaniu zapomniałem
, a z tą wydajnością w zapytaniach dla bazy to się średnio zgodzę. dlaczego? ponieważ jeśli użytkownik nie korzysta z jakiś skomplikowanych widoków z kilku tabel, nie ma bóg wie jakiego JOIN’nowania to wydajność naprawdę jest bardzo bardzo porównywalna. niemniej dzięki za zwrócenie uwagi na filtrowanie tego co otrzymujemy od użytkownika, pamięć mam dobrą tylko krótką
9 grudnia 2009 at 18:16
nie lepiej trzymać wszystko w jednej tabeli o strukturze drzewiastej? bo twoje rozwiązanie jest niezgodne z zasadami normalizacji
9 grudnia 2009 at 18:49
A ja się nie zgodzę z Tobą. Po pierwsze za każdym wywołaniem ponownie łączysz się z bazą danych, a to jest najbardziej czaso- i zasobożerna część pobierania danych. Więc sprawdzając możliwości wyboru – robisz 5 razy połączenie do bazy, a potem pobranie informacji.
18 grudnia 2009 at 17:16
„tylko takie rozwiązanie wydaje się trochę mało wydajne, ale mogę się mylić bo nigdy czegoś takiego nie stosowałem.”
No nie wiem co jest wydajniejsze. Czy za każdy onChange wykonywać zapytanie do bazy, czy załadować raz, a potem tylko wyświetlać właściwe wyniki?
22 grudnia 2009 at 19:57
Fajny wpis choć brak mi tu przykładu na jakimś frameworku js.
Co do pomysłu z pobieraniem wszystkiego do js zamiast odpytywania bazy przez ajaxa przy onChange to ktoś się chyba nie zastanowił … ostatnio robiłem coś w tym stylu i jakoś czarno widzę ładowanie paruset a obecnie to pewnie z 1-2 tyś wpisów do js bo może coś kogoś zainteresuje…
4 lutego 2010 at 15:15
Moim zdaniem, rozwiązanie świetne.
Za każdym razem odpytujemy bazę co daje nam pełną kontrolę i uniwersalność, dodatkowo wystarczy ponakładać indeksy na wyszukiwane pola.
Jeżeli ktoś robi tego rodzaju selecty to przecież nie podpina do tego tabel z milionami wierszy, najczęściej operuje na widoku, lub tabelach słownikowych.
Do małych serwisów IDEALNE.
Na minus moim zdaniem jest to, że tego selecta drugiego nie widać i że za każdym razem tworzymy nowy element blokowy DIV a przy 10 parametrach np przy generowaniu raportu może to być uciążliwe.
Oczywiście dostosowałem sobie to pod swoje potrzeby i dlatego wielkie 5 za ten artykuł.
Dużo wyciągnąłem z tego.
11 czerwca 2010 at 10:44
bardzo fajny skrypt podoba mi się i działa to najważniejsze. Duzo tego szukałem po necie i dopiero tutaj się udał więcej takich skryptów głowiłem się nad tym tematem sporo czasu. Dziękuje za gotowca i pozdrawiam ;D