Scriptorama.nl

Header image showing a keyboard, mouse, laptop and books on design patterns

Afhankelijke listboxes met PHP, MySQLi en Prototype

Je kent ze wel, een tweetal listboxes waarvan de 2e listbox automagisch nieuwe waarden krijgt wanneer je een nieuwe waarde selecteert in de eerste listbox. Vandaag op Scriptorama een korte tutorial over hoe je deze zelf kunt maken met behulp de Protoype javascript library

Let op: deze voorbeelden zijn bedoeld voor PHP 5! Om alles kort en bondig te houden wordt er niet zoveel gecontroleerd op fouten. Dit houdt natuurlijk niet in dat jij dat ook niet moet doen!

Voorbereidingen

Maak een allereerst een simpele directory structuur:

CODE:
  1. library/
  2. templates/
  3. javascripts/

Download nu de Prototype Javascript library en plaats het bestand prototype.js in de javascripts directory.

Gegevens verzamelen

Voordat we de listboxes kunnen maken zullen we eerst even wat testdata moeten regelen. Maak een database in MySQL met de volgende 2 tabellen en vul deze met wat data:

SQL:
  1. CREATE TABLE categorie (
  2.   cat_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  3.   naam VARCHAR(100) NOT NULL
  4. );
  5.  
  6. CREATE TABLE onderwerp (
  7.   id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  8.   cat_id INTEGER UNSIGNED NOT NULL,
  9.   naam VARCHAR(100)
  10. );
  11.  
  12. INSERT INTO categorie (naam) VALUES ('Browsers');
  13. INSERT INTO categorie (naam) VALUES ('Script talen');
  14. INSERT INTO categorie (naam) VALUES ('Database servers');
  15.  
  16. INSERT INTO onderwerp(cat_id, naam) VALUES (1, 'Mozilla FireFox');
  17. INSERT INTO onderwerp(cat_id, naam) VALUES (1, 'Opera');
  18. INSERT INTO onderwerp(cat_id, naam) VALUES (1, 'Safari');
  19.  
  20. INSERT INTO onderwerp(cat_id, naam) VALUES (2, 'PHP');
  21. INSERT INTO onderwerp(cat_id, naam) VALUES (2, 'Python');
  22. INSERT INTO onderwerp(cat_id, naam) VALUES (2, 'Ruby');
  23.  
  24. INSERT INTO onderwerp(cat_id, naam) VALUES (3, 'PostgreSQL');
  25. INSERT INTO onderwerp(cat_id, naam) VALUES (3, 'MySQL');
  26. INSERT INTO onderwerp(cat_id, naam) VALUES (3, 'Firebird');

Database toegang regelen

We zullen de MySQLi extensie gebruiken om de data uit de database te halen. Als je meer wilt lezen over MySQLi kun je terecht bij de volgende Scriptorama artikelen:

We maken een simpele klasse waarmee we de categorie en onderwerp gegevens kunnen ophalen:

library/categorie.class.php:

PHP:
  1. class CategorieGateway
  2. {
  3.   private $_db = null;
  4.  
  5.   function CategorieGateway($db)
  6.   {
  7.     $this->_db = $db;
  8.   }
  9.  
  10.   public function haalCategorien()
  11.   {
  12.     $res = $this->_db->query (
  13.       "SELECT id,naam FROM categorie"
  14.     );
  15.    
  16.     $resArray = array();
  17.  
  18.     while ( $row = $res->fetch_assoc() )   
  19.       $resArray[$row['id']] = $row['naam'];
  20.          
  21.     $res->free();
  22.  
  23.     return $resArray;
  24.   }
  25.  
  26.   public function haalOnderwerp($catId)
  27.   {
  28.     $resArray = array();
  29.     $catId = (int) $catId;
  30.     $res   = $this->_db->query (
  31.       "SELECT id,naam FROM onderwerp WHERE cat_id = $catId"
  32.     );
  33.  
  34.     while ( $row = $res->fetch_assoc() )
  35.       $resArray[$row['id']] = $row['naam'];
  36.  
  37.     $res->free();
  38.  
  39.     return $resArray;
  40.   }   
  41. }

Doordat we zeker weten dat we de onderdeel en categorie gegevens op 1 manier gaan gebruiken retourneren we de gegevens als een associatieve array, geindexeerd op database ID.

Deze associatieve arrays zijn handig als om snel een listbox mee te genereren maar dan hebben we nog wel een functie nodig die dat voor ons kan regelen:

library/ToListbox.php:

PHP:
  1. function toListbox($name, $data)
  2. {
  3.   ksort($data);
  4.   $res = '<select name="' . $name . '" id="'. $name . '">';
  5.  
  6.   foreach ($data as $id => $naam)
  7.   {
  8.     $res .= '<option value="' . $id . '">';
  9.     $res .= $naam;
  10.     $res .= '</option>';
  11.   }
  12.   $res .= "</select>";
  13.  
  14.   return $res;
  15. }

Deze functie lijkt me wel duidelijk. Hij zorgt er nog wel even voor dat de sleutels van de associatieve array gesorteerd zijn. Dat is handig voor als we met de hand nog iets toe willen voegen aan de data array. Dit zullen we later nog zien.

Deze twee bestanden en natuurlijk de connectie naar de database maken we vervolgens in een derde bestand:

init.php:

PHP:
  1. require 'library/ToListbox.php';
  2. require 'library/Categorie.class.php';
  3.  
  4. $db = new mysqli('localhost', 'scriptorama', 'test', 'listboxes');
  5.  
  6. if (mysqli_connect_errno()) {
  7.   echo "De database connectie is mislukt. Probeer het later nog eens.";
  8.   exit;
  9. }

De pagina met listboxes

De pagina haalt alleen de categorie gegevens op en maakt deze beschikbaar aan de template via de $template array. Dit is de enige variabele die de template zal proberen uit te lezen. Je ziet ook dat we een extra element toevoegen aan de data array. Dit is een kleine usability toevoeging en had dus geen plek in de database. Doordat we ksort() gebruiken in de toListbox() functie komt deze nieuwe waarde bovenaan te staan.

index.php

PHP:
  1. require './init.php';
  2.  
  3. $catGateway = new CategorieGateway($db);
  4. $categorie  = $catGateway->haalCategorien();
  5.  
  6. $categorie[0] = "Selecteer categorie";
  7.  
  8. $template = array (
  9.   'lbxCategorie' => ToListbox('categorie', $categorie)
  10. );
  11.  
  12. require './templates/index.tpl.php';

De template is simpel. We plaatsen de 2 listboxes allebei in een aparte div zodat we deze straks makkelijk kunnen manipuleren:

templates/index.tpl.php

PHP:
  1. <html>
  2.  <head>
  3.   <title>Scriptorama.nl - Afhankelijke listboxes</title>
  4.   <script src="javascripts/prototype.js"></script>
  5.   <script src="javascripts/listbox.js"></script>
  6.  </head>
  7.  <body>
  8.   <div id="lbx1">
  9.    <?php echo $template['lbxCategorie']; ?>
  10.   </div>
  11.  
  12.   <div id="lbx2">
  13.    <select id="onderwerp" name="onderwerp">
  14.     <option>Selecteer een onderwerp</option>
  15.    </select>
  16.   </div>
  17.  </body>
  18. </html>

Javascript, please!

Vervolgens zullen we een stukje javascript moeten schrijven dat er voor zorgt dat onze listbox geupdate wordt. We maken daarvoor een nieuw javascript bestand waarin we wat handigheden van Prototype zullen toepassen:

javascripts/listbox.js

JAVASCRIPT:
  1. function verversListbox(resp)
  2. {
  3.   if (resp.responseText != "-1")
  4.     Element.update($('lbx2'), resp.responseText);
  5. }
  6.  
  7. function verversListboxEvent()
  8. {
  9.     var catId = $F('categorie');
  10.     if (catId == 0) return;
  11.  
  12.     new Ajax.Request(
  13.         "onderwerp.php",
  14.         {
  15.             method: 'get',
  16.             parameters: "id=" + catId,
  17.             onComplete: verversListbox
  18.         }
  19.     );
  20. }
  21.  
  22. function attachHandlers()
  23. {
  24.   Event.observe (
  25.     $('categorie'),
  26.     'change',
  27.     verversListboxEvent,
  28.     false
  29.   );
  30. }
  31.  
  32. Event.observe(
  33.   window,
  34.   'load',
  35.   attachHandlers,
  36.   false
  37. );

Dit script definieert een tweetal functies en hangt de eventhandler aan de listbox. Op het moment dat er een andere waarde wordt geselecteerd in de listbox wordt de javascript functie verversListboxEvent() aangeroepen. Deze doet vervolgens een XMLHttpRequest naar onderwerp.php en geeft het geselecteerde categorie ID mee.

Wanneer dit XMLHttpRequest afgerond is wordt de functie verversListbox() aangeroepen welke ons tweede divje een nieuwe inhoud geeft: namelijk dat wat terug kwam van onderwerp.php! Tenzij de waarde -1 is, in welk geval er iets mis is gegaan in onderwerp.php.

Dit alles realiseren we met enkele features van Prototype:

  1. De functies $F() en $() zijn shorthand notaties voor alledaagse dingen in Javascript: $F('id') retourneert de waarde voor het form control met id 'id' en $('id') retourneert het HTML elementen met het id 'id'.
  2. Event.observe() laat je op simpele wijze een eventhandler aan een control of element hangen.
  3. Ajax.Request() voert een XMLHttpRequest uit naar het opgegeven bestand en met de opgegeven parameters
  4. Element.update() vervangt de HTML (innerHTML) van het opgeven HTML element met de nieuwe opgegeven HTML

Het enige wat nu nog mist is een script dat voor het opgegeven categorie ID de juiste lijst van onderwerpen ophaalt.

onderwerp.php:

PHP:
  1. require 'init.php';
  2.  
  3. if (!isset($_GET['id']) || !is_numeric($_GET['id']))
  4. {
  5.   echo "-1";
  6.   exit;
  7. }
  8.  
  9. $catGateway = new CategorieGateway($db);
  10.  
  11. $onderwerpen = $catGateway->haalOnderwerp($_GET['id']);
  12.  
  13. echo ToListbox('onderwerp', $onderwerpen);

Conclusie

Et voila, een simpele implementatie van afhankelijke listboxes. Er zijn nog verschillende andere methodes waarop dit gerealiseerd had kunnen worden. In plaats van de hele content van een div te vervangen hadden we ook JSON kunnen gebruiken of juist XML maar het is maar net wat je nodig hebt. In ons geval was dit de snelste en simpelste manier.

Prototype maakt het leven van de webdeveloper die met javascript werkt aanzienlijk makkelijker, bekijk ook de onofficiele handleiding eens!

Reageer ook!

Waarom de inhoud van de div vervangen?
Waarom uberhaupt een div rond de selects? Veel mooier is natuurlijk om de options array te legen en te vullen met de nieuwe options via DOM.

Omdat ik juist even snel (okay, okay, quick and enigzins dirty ;-) ) wilde laten zien wat je met prototype kunt doen. Het is inderdaad niet de mooiste manier.

Ik zal binnenkort nog een andere manier belichten waarbij JSON als data "protocol" gebruikt wordt. Deze methode zal dan inderdaad enkel de options van een listbox wijzigen.

Mathieu: heb je deze code ook getest onder Internet Explorer? Ik ben de afgelopen tijd ook bezig geweest met een soortgelijk iets, maar het lijkt erop dat 'change' op een selectbox niet werkt onder IE.. Onder FF uiteraard wel.

Berry: Ik geloof dat ik dit artikel wel getest heb onder Windows/MSIE. Ik maak ook gebruik van de prototype library die juist het verschil tussen de 2 browsers moet abstraheren.

Hoe dan ook, ik heb nog even een testje gemaakt en getest met MSIE 6 en dat werkt goed:

HTML:
  1. <title>test</title>
  2. </head>
  3.  
  4. <script src="/dev/scriptaculous/lib/prototype.js"></script>
  5.  
  6. <script type="text/javascript">
  7. <!--
  8. function test_Changed(e)
  9. {
  10.     alert('test');
  11. }
  12. function attachHandlers()
  13. {
  14.   Event.observe (
  15.     $('test'),
  16.     'change',
  17.     test_Changed,
  18.     false
  19.   );
  20. }
  21. Event.observe ( window, 'load', attachHandlers, false);
  22. -->
  23. </script>
  24.  
  25. <select id="test">
  26. <option value="1">Testerdetest</option>
  27. <option value="2">Test 2</option>
  28. <option value="3">Tester4</option>
  29. </select>
  30.  
  31. </body>
  32. </html>

Let op dat je als event 'change' gebruikt en niet 'onchange' ofzoiets.

Mocht het nu nog niet werken, post dan even je code in het forum, dan kan ik er verder naar kijken :)

Hallo,
ik ben net begonnen met Prototype en heb hierbij een paar vraagjes.
Waarom gebruik je niet

Dat is toch veel handiger, of ben ik mis?

Tweede punt, de Ajax.Request roept onderwerp.php aan. Kan hier ook een php functie aangeroepen worden ipv een php file. Zo blijft het wat overzichtelijk ipv al die kleine bestandjes.

Groeten, Stijn

Ik mis eigenlijk een sample wat uitlegt wat het nu eigenlijk is. Ben zelf meer van jQuery, dus kijk even verder. Duidelijk wel deze manier van uitleggen!

Leave a comment
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>