JavaScript/Tutorials/Verkettete Auswahllisten

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Informationen zum Autor

Name:
Rodney Rehm
E-Mail:
Homepage:
Beachten Sie: Bei Fragen zu diesem Beitrag kontaktieren Sie bitte den Autor.

Warum dieses Thema?[Bearbeiten]

Im modernen Webdesign wird immer mehr Wert auf Interaktivität gelegt. Um (statische) Webseiten zu dynamisieren, bietet sich die client-seitige Scriptsprache Javascript an. Für Browser, die diese Technologie unterstützen, lassen sich hilfreiche Erweiterungen programmieren. Verkettete Auswahllisten, bei welchen die Optionen der nächsten Auswahlliste in Abhängigkeit zur getroffenen Auswahl in der vohergehenden Auswahlliste stehen, lassen sich durchaus mit server-seitigen Scripts realisieren. Das macht es jedoch erforderlich, dass das Formular nach jeder Änderung der Auswahl an den Server geschickt und vom Server eine neu generierte Seite zurückgeliefert werden muss. Dabei muss je nach Architektur der Webseite auf einige Dinge Rücksicht genommen werden. Gegebenenfalls wurden weitere Formulardaten übermittelt. Oft werden Seiten aus vielen Bausteinen zusammengebastelt. Unzählige Daten wollen zusammengetragen werden (Dateien, Datenbank, Cache). Und so weiter. Kurz: "It's a pain in the ass". Mit ein wenig Javascript kann man diese Auswahlstrecken eleganter, benutzerfreundlicher und schneller realisieren.

Im Beispiel-Code wurde auf moderne Arbeitsweise geachtet. Es kommen DOM-Methoden (statt der möglichen Option-Methoden) zum Einsatz und es werden die Konzepte der objektorientierten Programmierung angewandt, welche für Javascript-Einsteiger vielleicht nicht sofort verständlich sind. Es wird jedoch versucht, möglichst alle verwendeten Konzepte zu erklären - ein kleiner Exkurs in die Welt der objektorientierten Programmierung ist also mit von der Partie.

Nach einem kurzen Blick auf das Endprodukt werden die verwendeten Funktionen und Techniken erläutert. Für den direkten Einsatz des Beispiel-Codes sind lediglich die Bereiche Das Format der Optionsdaten, Die Callback-Funktion und die Beispielimplementierung wichtig.

Beispielimplementierung[Bearbeiten]

Beispiel ansehen …
<!DOCTYPE html> <html> <head> <meta charset = "utf-8"> <title>Verkettete Auswahllisten Beispiel 1</title> <script src="terminAuswahl.js"></script> <script src="LinkedSelection.js"></script> <script> /** * Diese Callback-Funktion soll beim Abschliessen der Auswahlstrecke und beim * ändern der Auswahl in einer der Auswahllisten aufgerufen werden. * @param selected ein Array mit den Daten der jeweiligen Auswahllisten * (leer, wenn Strecke nicht abgeschlossen) **/ function ergebnisZeigen( selected ) { if( selected.length ) { /* * Auswahlstrecke wurde beendet */ // Visualisierung der Auswahlsrecke var sel = ''; var val = ''; var txt = ''; for( var i = 0; i < selected.length; i++ ) { sel += ( i>0 ? ' → ' : '') + selected[i].id; val += ( i>0 ? ' → ' : '') + selected[i].value; txt += ( i>0 ? ' → ' : '') + selected[i].text; } var output = '<h3>Gewählte Strecke:<'+'/h3><h4>IDs der Auswahllisten<'+'/h4><p>' + sel + '<'+'/p><h4>Werte der gewählten Optionen<'+'/h4><p>' + val + '<'+'/p><h4>Texte (Namen) der gewählten Optionen<'+'/h4><p>' + txt + '<'+'/p>'; } else { /* * Auswahlstrecke wurde noch nicht beendet */ // Hinweis zur Auswahl in der nächsten Auswahlliste var output = '<p>Wählen Sie eine Option aus der nächste Auswahlliste.<'+'/p>'; } var ergebnisObj = document.getElementById( 'ergebnis' ).innerHTML = output; }; window.onload = function() { var vk = new LinkedSelection( [ 'professor', 'lesung', 'termin' ], ergebnisZeigen, terminAuswahl ); } </script> </head> <body> <h1>Beispiel 1</h1> <p>Bitte wählen Sie in der folgenden Auswahlliste einen Professor</p> <p> <label id="professorLabel" for="professor">Professor:</label> <select id="professor"> <option value="--">Bitte wählen:</option> <option value="p23">Albers, Alfred</option> <option value="p24">Braun, Berta</option> <option value="p25">Drachenzaun, Doris</option> <option value="p26">Müller, Stephan</option> <option value="p27">Meier, Manfred</option> </select> <label id="lesungLabel" for="lesung">Lesung:</label> <select id="lesung"> <option value="--">------</option> </select> <label id="lesungTermin" for="termin">Termin:</label> <select id="termin"> <option value="--">------</option> </select> </p> <div id="ergebnis"></div> </body> </html>

Die wichtigsten Punkte der Einbindung im überblick:

  • Die Javascript-Datei terminAuswahl.js (Daten für die Auswahllisten) einbinden
  • Die Javascript-Datei LinkedSelection.js einbinden (
  • Eine Callback-Funktion definieren
  • Die Auswahllisten definieren
  • LinkedSelection instanzieren

Der Kern der verketteten Auswahllisten ist die Datei LinkedSelection.js, in ihr befindet sich das LinkedSelection-Objekt. Um dieses Beispiel in einen produktiven Einsatz zu bringen, braucht diese Datei nicht bearbeitet zu werden. Die Komponenten des LinkedSelection-Objekts werden in den folgenden Bereichen erläutert. Die Datei terminAuswahl.js enthält alle möglichen Optionen für die Auswahllisten. Eine Callback-Funktion muss deklariert werden, welche von der Instanz des LinkedSelection-Objekts aufgerufen werden kann, wenn sich an der Auswahl etwas ändert. Die Auswahllisten müssen definiert werden, dabei muss die erste Auswahlliste bereits mit Optionen befüllt werden. Indem wir die Instanzierung des LinkedSelection-Objekts im onload-Handler ausführen, stellen wir sicher, dass die Auswahlstrecke erst gestartet wird, wenn die komplette Seite geladen wurde. Andernfalls könnte das Javascript versuchen, auf die Auswahllisten zuzugreifen, bevor die Deklaration dieser beim Browser angelangt ist. Das LinkedSelection-Objekt erwartet 3 Parameter: ein Array mit den IDs der Auswahllisten, die Callback-Funktion, das Datenobjekt.

Hilfsfunktionen[Bearbeiten]

Es gibt einige Abläufe im Umgang mit Auswahllisten, die sich sehr schön in (wiederverwendbare) Funktionen verpacken lassen. Die im Folgenden vorgestellten Funktionen bedienen sich des Objekts this. this referenziert (zeigt auf) das Objekt, in dessen Kontext die Funktion gerade ausgeführt wird. Die folgenden Hilfsfunktionen funktionieren also nur dann, wenn wir sie den Auswahllisten als neue Objekt-Methoden anfügen. Man darf sich hier nicht in die Irre führen lassen; obwohl die Funktionen nicht in einem Objekt deklariert werden, wird this später auf richtige Objekt zugreifen, nämlich dasjenige Objekt, welchem wir die neuen Funktionen zuweisen.

Beispiel: Auslesen des Wertes der aktuell gewählten Option
var selectedValue = function()
{
  return this.options[ this.selectedIndex ].value;
};
selectedValue gibt den Wert (value) der gewählten Option zurück.
Beispiel: Auslesen des Textes der aktuell gewählten Option
var selectedText = function()
{
  return this.options[ this.selectedIndex ].text;
};
selectedText gibt statt des Wertes (value), den Namen (text) der gewählten Option zurück.
Beispiel: Auswählen einer Option anhand des Wertes
var selectByValue = function( value )
{
  for( var i = 0; i < this.options.length; i++ )
  {
    if( this.options[i].value == value )
      this.selectedIndex = i;
  }
};
selectByValue durchläuft alle Optionen und selektiert die letzte Option, deren Wert mit dem Parameter value übereinstimmt.
Beispiel: Ersetzen der Optionen
var replaceOptions = function( newOptions )
{
  // alte Optionen der Auswahlliste löschen
  var opts = this.getElementsByTagName( 'option' );
  while( opts.length > 0 )
    this.removeChild( opts[0] );

  // wenn keine neuen Optionen übergeben wurden, default-Option setzen
  // andernfalls "Bitte wählen" voranstellen
  if( newOptions.length == 0)
    this.addOption( '--', '------' );
  else
    this.addOption( '--', 'Bitte wählen:' );

  // neue Optionen in die Auswahlliste schreiben
  for( var i = 0; i < newOptions.length; i++ )
    this.addOption( newOptions[i][0], newOptions[i][1] );
};
replaceOptions erwartet einen Parameter, welcher ein Array sein muss. Jedes Element ist wiederum ein Array mit 2 Elementen. Das erste Element enthält den Wert (value) der Option, das zweite Element den Namen (text). replaceOptions löscht zunächst alle Optionen, die die Auswahlliste besitzt. Das übergebene Array newOptions wird dann durchlaufen und für jedes Element eine neue Option in die Auswahlliste gehängt. Wenn ein leeres Array übergeben wurde, wird die Default-Option (------) gesetzt.
Beispiel: Hinzufügen einer neuen Option
var addOption = function( value, text )
{
  var opt = document.createElement( 'option' );
  opt.value = value;
  opt.appendChild( document.createTextNode( text ) );
  this.appendChild( opt );
};
addOption erwartet zwei Parameter. Der erste wird als Wert der neuen Option gesetzt, der zweite ist der Name der neuen Option.
Beachten Sie: this beschreibt (referenziert) das Objekt, in dessen Kontext die Funktion ausgeführt wird. Würde diese Funktion einfach so aufgerufen werden, würde this eine Referenz auf das window-Objekt sein und einen Fehler verursachen, da das window-Objekt nicht über die Attribute options und selectedIndex verfügt. Weisen wir diese Funktionen einer Auswahlliste zu, so ist this eine Referenz auf die Auswahlliste selbst, welche sehr wohl über die genannten Attribute verfügt. var funktionsName = function(){ ... } bewirkt das gleiche wie function funktionsName(){ ... }. Da wir unsere Funktionen aber (sinnvoll) kapseln wollen, wählen wir diese „Privat-Funktion“-Notation.

Die Callback-Funktion[Bearbeiten]

Eine Callback-Funktion (Rückruffunktion) unterscheidet sich zunächst nicht von einer normalen Funktion. Im Gegensatz zu anderen Funktionen wird sie jedoch nicht sofort ausgeführt, sondern als Parameter einer anderen Funktion übergeben, um innerhalb dieser benutzt werden zu können.

Beispiel: Beispiel zum allgemeinen Einsatz von Callback-Funktionen
/*
 * Eine Funktion, die über das übergebe
 * Array iteriert und für jedes Element die
 * Callback-Funktion ausführt
 */
function fuerJedesElement( callback, array )
{
  for( var i = 0; i < array.length; i++ )
  {
    callback( array[i] );
  }
}

/*
 * eine mögliche Callback-Funktion,
 * welche das Quadrat aller Werte ausgeben soll
 */
function callback_quadrat( wert )
{
  alert( wert*wert );
}

/*
 * eine mögliche Callback-Funktion,
 * welche ein entsprechendes Element aus einem
 * anderen Array ausgeben soll
 */
function callback_text( wert )
{
  var daten = new Array( 'Null', 'Eins', 'Zwei', 'Drei', 'Vier' );
  alert( (!daten[wert]) ? 'unbekannt' : daten[wert] );
}

/*
 * Ausgabe des folgenden Aufrufs:
 * 0, 1, 4, 9, 16
 */
fuerJedesElement( <strong>callback_quadrat</strong>, [0,1,2,3,4] );

/*
 * Ausgabe des folgenden Aufrufs:
 * Null, Eins, Zwei, Drei, Vier, unbekannt
 */
fuerJedesElement( <strong>callback_text</strong>, [0,1,2,3,4,5] );

Wir rufen die Funktion fuerJedesElement mit zwei unterschiedlichen Callback-Funktionen auf und erhalten, je nach Funktionsweise der Callback-Funktion, unterschiedliche Resultate. fuerJedesElement durchläuft das übergebene Array und führt die angegebene Callback-Funktion für jedes Element aus.

Für die Anwendung des Callback-Prinzips muss man beachten, dass auch Funktionen in Javascript Objekte sind. Objekte (Variablen) lassen sich als Parameter an Funktionen übergeben und können bspw. andere Funktionen durch eine simple Zuweisung überschreiben.

Die Callback-Funktion im LinkedSelection-Objekt[Bearbeiten]

Das Callback-Prinzip lässt sich wunderbar als Eventhandler einsetzen. In einem Objekt wird eine Funktion registriert, welche ausgeführt wird, wenn irgendwas passiert. Genau dies machen wir uns für unser LinkedSelection-Objekt zu Nutze. Wenn der Benutzer in einer Auswahlliste eine andere Option wählt, soll diese Funktion aufgerufen werden, damit wir entsprechend reagieren können.

Auf den ersten Blick würde es wohl reichen, wenn wir die Callback-Funktion nur dann aufrufen, wenn in der letzten Auswahlliste ein gültiger Wert ausgewählt wird. Wir möchten aber auch reagieren können, wenn der Benutzer die Auswahl in einer vorhergehenden Liste ändert, damit wir die Ausgabe entsprechend anpassen können.

Beispiel: function ergebnisZeigen
function ergebnisZeigen( selected )
{
  if( selected.length )
  {
    /*
     * Auswahlstrecke wurde beendet
     */

    // Visualisierung der Auswahlstrecke
    var sel = '';
    var val = '';
    var txt = '';
    for( var i = 0; i < selected.length; i++ )
    {
      sel += ( i>0 ? ' &amp;rarr; ' : '') + selected[i].id;
      val += ( i>0 ? ' &amp;rarr; ' : '') + selected[i].value;
      txt += ( i>0 ? ' &amp;rarr; ' : '') + selected[i].text;
    }
    var output = '<h3>Gewählte Strecke:</h3><h4>IDs der Auswahllisten</h4><p>' + sel +
           '</p><h4>Werte der gewählten Optionen</h4><p>' + val +
           '</p><h4>Texte (Namen) der gewählten Optionen</h4><p>' + txt + '</p>';
  }
  else
  {
    /*
     * Auswahlstrecke wurde noch nicht beendet
     */

    // Hinweis zur Auswahl in der nächsten Auswahlliste
    var output = '<p>Wählen Sie eine Option aus der nächsten Auswahlliste.'</p>';
  }
  var ergebnisObj = document.getElementById( 'ergebnis' ).innerHTML = output;
};

Die Callback-Funktion wird bei jeder Änderung der Auswahl bei egal welcher Auswahlliste der Auswahlstrecke aufgerufen. Wird in der letzten Auswahlliste der Strecke ein gültiger Wert ausgewählt, so wird ein nicht-leeres Array an die Callback-Funktion übergeben. In allen anderen Fällen wird die Callback-Funktion mit einem leeren Array für den Parameter selected aufgerufen. Ist die Anzahl der Elemente in selected 0, so können wir mit Sicherheit sagen, dass die Auswahlstrecke noch nicht komplett durchlaufen wurde und wir eine entsprechende Meldung anzeigen möchten. Ist die Strecke beendet worden, so bekommt die Callback-Funktion ein Array übergeben, welches genau so viele Elemente besitzt, wie es Auswahllisten in der Strecke gibt. Für dieses Beispiel arbeiten wir die Daten für eine Visualisierung auf.

Beachten Sie: Wurde die Auswahlstrecke noch nicht beendet, wird selected ein leeres Array beinhalten.

Wenn die Auswahlstrecke absolviert wurde, wird für jede Auswahlliste ein Element im Array selected gesetzt. Man könnte das Array auch Referenzen auf die jeweiligen Auswahllisten enthalten lassen, aus Gründen der Datenkapselung möchte man dies jedoch vermeiden. Jedes der Arrayelemente ist ein Objekt mit den folgenden Attributen:

Attribut Wert
id die ID der Auswahlliste
value der Wert der gewählten Option
text die Bezeichnung der gewählten Option

Der ChangeHandler[Bearbeiten]

Ein wesentlicher Teil der verketteten Auswahllisten ist die Fähigkeit, auf die Veränderung der Auswahl zu reagieren. Wenn der Benutzer in einer Auswahlliste eine Option wählt, müssen wir reagieren können, z.B. damit die nächste Auswahlliste mit Optionen befüllt wird. Auswahllisten haben von Haus aus eine Methode namens onchange. Diese wird aufgerufen, sobald die Auswahl geändert wird. onchange können wir eine neue Funktion zuweisen, und zwar nach dem gleichen Prinzip, das bei den Hilfsfunktionen erläutert wurde.

Beispiel: der ChangeHandler
var changeHandler = function()
  {
    var value = this.selectedValue();

    // Auf die nächste Auswahlliste folgende Auswahllisten müssen wieder
    // in den default-Zustand versetzt werden
    if( typeof(this.nextSelect) == 'object' )
    {
      for( var i = this.nextSelect.selectID + 1; i < selects.length; i++ )
        selects[i].replaceOptions( new Array() );
    }

    // Abbrechen, wenn ein Dummy-Wert ausgewählt wurde
    if( value == '--' )
    {
      if( this.selectID < selects.length )
        selects[ this.selectID +1 ].replaceOptions( new Array() );

      return;
    }

    if( typeof(this.nextSelect) == 'object' )
    {
      /*
       * nextSelect ist eine Auswahlliste
       */

      // Wenn keine Daten zur gemachten Auswahl zur Verfügung stehen,
      // müssen wir sicherstellen, dass wir auf keine nicht vorhandenen Objekte zugreifen.
      if( !data[ this.nextSelect.id ][ value ] )
      {
        if( !data[ this.nextSelect.id ] )
          data[ this.nextSelect.id ] = {};

        data[ this.nextSelect.id ][ value ] = new Array();
      }

      // Neue Optionen in der nächsten Auswahlliste setzen
      this.nextSelect.replaceOptions( data[ this.nextSelect.id ][ value ] );

      // Wenn die Auswahlstrecke nicht beendet ist, muss die Callback-Funktion
      // dennoch aufgerufen werden, damit entsprechend auf änderungen
      // reagiert werden kann.
      callback( new Array() );
    }
    else
    {
      /*
       * Die Auswahlstrecke ist absolviert
       */

      // Wahlen der einzelnen Listen in ein Array schreiben um
      // dieses an die Callback-Funktion zu übergeben.
      var selected = new Array();
      for( var i = 0; i < selects.length; i++ )
      {
        selected.push( { 'id' : selects[i].id,
                 'value': selects[i].selectedValue(),
                 'text' : selects[i].selectedText() } );
      }
      callback( selected );
    }
  };

Statt für jede Auswahlliste eine eigene onchange-Funktion zu schreiben, bauen wir eine einzige Funktion, die mit allen Auswahllisten gleichermaßen klar kommt. Wenn eine Default-Option (------) gewählt wurde, soll der Vorgang abgebrochen werden. Wenn das Attribut nextSelect keine Referenz auf eine Auswahlliste enthält, ist die Auswahlstrecke absolviert. In diesem Fall müssen alle Auswahllisten durchlaufen und die gewählten Optionen notiert werden. Mit diesen Daten wird dann die Callback-Funktion aufgerufen. Handelt es sich bei nextSelect um ein Objekt (eine Auswahlliste), so muss dieses mit neuen Optionen befüllt werden. Um Aufwand zu sparen, prüfen wir, ob es für die aktuell gewählte Option überhaupt Daten in daten gibt. Wenn nicht, legen wir ein leeres Objekt an, damit die nachfolgenden Funktionen nicht auf nicht-deklarierte Variablen zugreifen. Dank der Initialisierungsfunktion des LinkedSelection-Objekts verfügen alle Auswahllisten über die Methode replaceOptions, welche im Abschnitt Hilfsfunktionen erläutert wurde. Dieser übergeben wir die relevanten Daten aus dem daten-Objekt. Die Struktur des daten-Objektes wird unter Format der Optionsdaten erklärt. Für folgende Auswahllisten in der Kette müssen möglicherweise bereits geschriebene Optionen zurückgesetzt werden, das erreichen wir durch den simplen Aufruf von .replaceOptions( new Array() );. Wir geben alle Events an die Callback-Funktion weiter, im Falle der nicht vollendeten Auswahlstrecke mit Übergabe eines leeren Arrays}

Beachten Sie: Die in dieser Funktion nicht deklarierten Attribute nextSelect, data und callback haben ihren Ursprung in der Initialisierungsfunktion des LinkedSelection-Objekts.

Das Format der Optionsdaten[Bearbeiten]

Für dieses Beispiel werden die möglichen Optionen aller Auswahllisten der Auswahlkette in einem JSON-Objekt gespeichert. Dabei werden die folgenden Konventionen umgesetzt:

  • Alle Optionen der Auswahllisten werden in einem zentralen Objekt notiert.
  • Alle Optionen einer Auswahlliste werden in einem Objekt notiert, welches den Namen bekommt, den die Auswahlliste als ID gesetzt hat.
  • Alle Optionen einer Auswahlliste werden in einem Array notiert, dessen Name den Wert der Option der vorhergehenden Auswahlliste bekommt, zu welchem es in Abhängigkeit steht.
  • Jede Option (Arrayelement) ist wiederum ein (zweielementiges) Array, dessen erstes Element den Wert (value) und das zweite Element den Namen (text) der Option enthält.
Beispiel: beispielhafte Optionsdaten
var terminAuswahl = { 'professor' : { 'p23' : ['p23', 'Albers, Alfred'] }, 'lesung' : { 'p23' : [ ['l55', 'Katzen in der freien Wildbahn'], ['l56', 'Katzen im Gefängnis'], ['l57', 'Katzen in Australien'] ] }, 'termin' : { 'l55' : [ ['t123', 'freitags 10 - 12'], ['t124', 'sonntags 18 - 20'] ], 'l56' : [ ['t125', 'montags 12 - 14'], ['t126', 'dienstags 8 - 10'] ], 'l57' : [ ['t127', 'dienstags 10 - 12'], ['t128', 'mittwochs 13 - 15'] ] } };

Analog zur oben genannten Konvention zur Datenformatierung gibt es das zentrale Datenobjekt terminAuswahl. Die Objekte professor, lesung und termin sind die Pendants zu den Auswahllisten. Die Namen der Objekte stimmen mit den jeweiligen IDs der Auswahllisten (#professor, #lesung, #termin) überein. Diese Objekte umfassen für jede mögliche Wahl in der vorhergehenden Auswahlliste ein Attribut. Die Namen der Attribute sind jeweils die Werte der Optionen in der vorhergehenden Auswahlliste. Hierdurch wird eine Abhängigkeit in einer flachen Hierarchie dargestellt. Die Attribute sind jeweils Arrays, welche die Optionen für die Auswahlliste bereithalten. Jedes dieser Arrays besitzt zwei Elemente, wobei das erste den Wert (value) und das zweite den Namen (text) der Option enthält.

Beachten Sie: Auswahllisten mit doppelten Optionen, oder mehreren Optionen mit dem gleichen Wert sind mit dieser Struktur nicht möglich. Sollte dies eine Anforderung sein, bieten die Möglichen Variationen einige hilfreiche Ansätze.

Das LinkedSelection-Objekt[Bearbeiten]

In modernen Programmen wird sehr viel Wert auf Datenkapselung und Namensräume gelegt. Oft werden Variablen verwendet, auf die nicht direkt von außen zugegriffen werden soll. Möchte man diese Variablen auslesen oder verändern lassen, stellt man entsprechende Funktionen zur Verfügung. Im Großen und Ganzen ist das auch schon die ganze Geschichte der Kapselung. Namensräume sind vorallem dann unerlässlich, wenn viele verschiedene Komponenten zusammenspielen müssen. Es könnte sein, dass zwei Komponenten die Funktionen selectedValue besitzen, die aber unterschiedlich funktionieren. Woher soll das Programm wissen, welche der beiden gleichnamigen Funktionen nun die richtige ist? In Javascript können wir uns mittels der Kapselung von Funktionen Namensräume schaffen. Die oben vorgestellten Hilfsfunktionen und der changeHandler sind nur im Kontext der verketteten Auswahllisten brauchbar, weshalb wir sie im Objekt LinkedSelection "verstecken". In den vorhergehenden Abschnitten wurden die einzelnen Komponenten des LinkedSelection-Objekts erläutert. Im folgenden Codeblock wird zudem die Funktion init vorgestellt, welche für das Auffinden und Vorbereiten der Auswahllisten zuständig ist.

Beispiel: das LinkedSelection-Objekt
/**
 * LinkedSelection ist ein Klasse zur Steuerung dynamisch verketteter Auswahllisten
 * @param inputSelects ein Array mit den IDs der Auswahllisten in hierarchischer Reihenfolge
 *            Bsp: [ 'select1', 'select2', 'select3' ]
 * @param callback Funktion, welche beim Abschließen (und ändern) der Auswahl aufgerufen werden soll
 * @param data das Daten-Objekt in JSON
 *            Bsp: { 'select1':['wert1','text1'], 'select2':['wert5','text5'] }
 **/
function LinkedSelection( inputSelects, callback, data )
{
  var self = this;        /* um aus EventHandlern auf diese Instanz zugreifen zu können */
  var selects = new Array();    /* Liste der verketteten Auswahllisten */

  /**
   * Die Funktion changeHandler wird dem onchange-Handler jeder Auswahlliste zugewiesen.
   * Wenn eine gültige Auswahl getroffen wurde, soll entweder die als nächste
   * Auswahlliste (nextSelect) bekannte Auswahlliste mit Daten befüllt werden,
   * oder die Callback-Funktion ausgeführt werden.
   **/
  var changeHandler = function()
  {
    var value = this.selectedValue();

    // Auf die nächste Auswahlliste folgende Auswahllisten müssen wieder
    // in den default-Zustand versetzt werden
    if( typeof(this.nextSelect) == 'object' )
    {
      for( var i = this.nextSelect.selectID + 1; i < selects.length; i++ )
        selects[i].replaceOptions( new Array() );
    }

    // Abbrechen, wenn ein Dummy-Wert ausgewählt wurde
    if( value == '--' )
    {
      if( this.selectID < selects.length )
        selects[ this.selectID +1 ].replaceOptions( new Array() );

      return;
    }

    if( typeof(this.nextSelect) == 'object' )
    {
      /*
       * nextSelect ist eine Auswahlliste
       */

      // Wenn keine Daten zur gemachten Auswahl zur Verfügung stehen,
      // müssen wir sicherstellen, dass wir auf keine nicht vorhandenen Objekte zugreifen.
      if( !data[ this.nextSelect.id ][ value ] )
      {
        if( !data[ this.nextSelect.id ] )
          data[ this.nextSelect.id ] = {};

        data[ this.nextSelect.id ][ value ] = new Array();
      }

      // Neue Optionen in der nächsten Auswahlliste setzen
      this.nextSelect.replaceOptions( data[ this.nextSelect.id ][ value ] );

      // Wenn die Auswahlstrecke nicht beendet ist, muss die Callback-Funktion
      // dennoch aufgerufen werden, damit entsprechend auf änderungen
      // reagiert werden kann.
      callback( new Array() );
    }
    else
    {
      /*
       * Die Auswahlstrecke ist absolviert
       */

      // Auswahlwerte der einzelnen Listen in ein Array schreiben um
      // dieses an die Callback-Funktion zu übergeben.
      var selected = new Array();
      for( var i = 0; i < selects.length; i++ )
      {
        selected.push( { 'id' : selects[i].id,
                 'value': selects[i].selectedValue(),
                 'text' : selects[i].selectedText() } );
      }
      callback( selected );
    }
  };

  /**
   * replaceOptions ersetzt die aktuellen Optionen der Auswahlliste durch
   * die im Array newOptions gelieferten Daten. Wenn ein leeres Array übergeben
   * wird, wird die default-Option "--" gesetzt.
   * @param newOptions ein Array mit den neuen Optionen
   *            Bsp: [ ['value1','text1'], ['value2','text2'], ]
   **/
  var replaceOptions = function( newOptions )
  {
    // alte Optionen der Auswahlliste löschen
    var opts = this.getElementsByTagName( 'option' );
    while( opts.length > 0 )
      this.removeChild( opts[0] );

    // wenn keine neuen Optionen übergeben wurden, default-Option setzen
    // andernfalls "Bitte wählen" voranstellen
    if( newOptions.length == 0)
      this.addOption( '--', '------' );
    else
      this.addOption( '--', 'Bitte wählen:' );

    // neue Optionen in die Auswahlliste schreiben
    for( var i = 0; i < newOptions.length; i++ )
      this.addOption( newOptions[i][0], newOptions[i][1] );
  };

  /*
   * Fügt der Auswahlliste eine neue Option hinzu
   * @param value Wert der neuen Option
   * @param text Name der neuen Option
   */
  var addOption = function( value, text )
  {
    var opt = document.createElement( 'option' );
    opt.value = value;
    opt.appendChild( document.createTextNode( text ) );
    this.appendChild( opt );
  };

  /**
   * holt den Wert der aktuell gewählten Option
   * @returns den Value der aktuell gewählten Option
   **/
  var selectedValue = function()
  {
    return this.options[ this.selectedIndex ].value;
  };

  /**
   * holt den Text (Name) der aktuell gewählten Option
   * @returns den Text der aktuell gewählten Option
   **/
  var selectedText = function()
  {
    return this.options[ this.selectedIndex ].text;
  };

  /**
   * Selektiere die Option mit dem Wert value, wenn keine Option mit dem Wert
   * value existiert, wird die Auswahl nicht geändert.
   * @param value der Wert den eine Option haben muss, um ausgewählt zu werden.
   **/
  var selectByValue = function( value )
  {
    for( var i = 0; i < this.options.length; i++ )
    {
      if( this.options[i].value == value )
        this.selectedIndex = i;
    }
  }

  /**
   * Initialisiere den Manager für verkettete Auswahllisten.
   * Findet Auswahllisten anhand der (per inputSelects) bekannten IDs.
   * Bestückt die Auswahllisten mit den nötigen Funktionen und Event-Handlern
   **/
  this.init = function()
  {
    // bestücke bestehende selects
    for( var i = 0; i < inputSelects.length; i++ )
    {
      var t = document.getElementById( inputSelects[i] );

      // ignoriere falsche IDs
      if(!t)
        continue;

      // neue Funktionen und Event-Handler zuweisen und in selects registrieren
      t.replaceOptions = replaceOptions;
      t.addOption = addOption;
      t.selectedValue = selectedValue;
      t.selectedText = selectedText;
      t.selectByValue = selectByValue;
      t.selectID = selects.length;
      t.onchange = changeHandler;
      selects.push( t );

      // registriere Auswahlliste als nextSelect bei der vorhergehenden
      if( selects.length > 1 )
        selects[ selects.length-2 ].nextSelect = t;
    }
  };

  // initialisieren!
  this.init();
}

Bis auf init wurden bereits alle Komponenten in den vorhergehenden Abschnitten dieses Artikels behandelt und erläutert. Die Funktion init wird im LinkedSelection-Objekt als Konstruktor verwendet. D.h. init wird beim Instanzieren des Objekts ausgeführt. Die Funktion durchläuft das übergebene Array, das die IDs der Auswahllisten enthält. Zu jeder ID wird die Auswahlliste im DOM-Baum gesucht. Wird zu einer ID kein Element gefunden, wird sie schlicht ignoriert. Den gefundenen Elementen werden dann die neuen Funktionen replaceOptions, addOption, selectedValue, selectedText und selectByValue angehängt. Das Wichtigste, der changeHandler, wird ebenfalls dem Element (der Auswahlliste) zugewiesen. Jede Auswahlliste muss ihren Nachfolger in der Auswahlkette kennen. Die aktuell behandelte Auswahlliste wird dabei dem Vorgänger im Attribut nextSelect bekannt gemacht, sofern ein Vorgänger existiert. Alle Auswahllisten werden zudem im internen Array selects registriert.

Empfehlung: Die vorgestellten Funktionen changeHandler und init setzen auf das Konzept der Closures, welche im Artikel Organisation von JavaScripten ausführlich behandelt werden. Dieser Artikel erläutert auch viele andere z.T. auch hier verwendete Konzepte der modernen Javascript-Programmierung. Nehmen Sie sich die Zeit und schenken Sie den dort vorgestellten Gedanken ein wenig Aufmerksamkeit.

Mögliche Variationen[Bearbeiten]

Das vorgestellte Beispiel verzichtet bewusst auf den Einsatz von AJAX und damit auch auf viele alternative Möglichkeiten der Datenspeicherung. Es ist (vor allem für längere Ketten mit vielen Auswahlmöglichkeiten) sinnvoll, die Daten der Optionen der nächsten Auswahlliste per AJAX zu laden. Das Konzept AJAX hier vorzustellen hätte den Rahmen wohl endgültig gesprengt. Dennoch sollen einige kurze Hinweise gegeben werden, wie das Beispiel mit dynamischen Daten arbeiten kann.

Datenbeschaffung per AJAX[Bearbeiten]

Um die Macht von AJAX nutzen zu können, muss der changeHandler etwas angepasst werden. Statt data als Datenobjekt zu behandeln, könnte data auch eine Funktion sein. Sie würde ähnlich der Callback-Funktion bei der Instanzierung des LinkedSelection-Objektes als Parameter übergeben werden. Diese Funktion würde den HTTP-Request starten. Beim Aufrufen der Funktion könnte neben dem Wert der aktuellen Auswahlliste, die nächste Auswahlliste übergeben werden. Diese wird dann direkt von der HTTP-Response-verarbeitenden Funktion manipuliert werden.

Datenformatierung bei AJAX[Bearbeiten]

Neben zielgenauer Datenbeschaffung bietet AJAX die Möglichkeit, Daten in verschiedenen Formaten zu beschaffen. Es muss nicht unbedingt ein JSON-Objekt sein. XML-Dokumente sind ebenso denkbar wie fertiges HTML, welches einfach per selectObj.innerHTML eingefügt wird.

Auswahllisten-Objekte dynamisch erstellen[Bearbeiten]

In diesem Beispiel wird vorausgesetzt, dass alle verwendeten Auswahllisten im HTML-Quelltext notiert und die erste Auswahlliste bereits mit Optionen befüllt wurde. Dynamischer wäre es, würde man die Auswahllisten bei der Ausführung des changeHandlers erstellen. Dadurch würde man sich zwar die Notation der Auswahllisten im HTML-Quelltext ersparen, würde aber die Möglichkeit, Javascript-inkompatible Browser zu unterstützen, verlieren.

Auswahllisten dynamisch erscheinen lassen[Bearbeiten]

Eine leere Auswahlliste sieht weder besonders schön aus, noch hat sie eine Funktion. Es wäre denkbar die abhängigen (auf die erste Auswahlliste folgenden) Auswahllisten mittels CSS zu verstecken und erst bei Bedarf erscheinen zu lassen. Dies hat zudem einen visuell größeren Effekt, als das simple Nachladen von Optionen. Diese visuellen Effekte sind wichtig, nicht um schön auszusehen, oder gar überladen zu wirken, sondern um den Benutzer auf eine Änderung aufmerksam zu machen.

Hinweise und Kurioses[Bearbeiten]

Die folgenden Hinweise beziehen sich nicht nur auf die in diesem Artikel vorgestellten Themen und Beispiele. Es handelt sich Probleme, die meist nicht sonderlich intuitiv sind, aber gerade bei Anfängern öfter für heftige Kopfschmerzen sorgen.

Korrekte Syntax bei JSON[Bearbeiten]

Interessant ist ebenfalls, dass sich Firefox bei JSON-Daten nicht wegen überzähliger Kommas (var data = { 'hello':'world', };) beschwert, wohingegen der Internet Explorer die Ausführung umgehend abbricht. Sind diese Daten (wie in unserem Beispiel oben) in einer eigenen Datei notiert, sucht man den Fehler in Ermangelung einer brauchbaren Fehlermeldung unter Umständen eine lange Zeit. Aus diesem Grund sollte immer peinlich genau darauf geachtet werden, dass sich keine Kommas nach den letzten Elementen der Objekt- und Arraydeklarationen verstecken.