JavaScript/Tutorials/Formulare/Verkettete Auswahllisten

Aus SELFHTML-Wiki
< JavaScript‎ | Tutorials‎ | Formulare(Weitergeleitet von Verkettete Auswahllisten)
Wechseln zu: Navigation, Suche

Auswahllisten mit select sind HTML-Formularelemente, die eine Liste von Werten einer fest definierten Menge in einem Dropdown zur Auswahl bereitstellen. Verkettete Auswahllisten, bei welchen die Optionen der nächsten Auswahlliste in Abhängigkeit zur getroffenen Auswahl in der vorhergehenden Auswahlliste stehen, lassen sich durchaus mit serverseitigen 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".

In diesem Kapitel lernen Sie, wie Sie diese Auswahlstrecken eleganter, benutzerfreundlicher und schneller realisieren.

Beachten Sie, bitte, dass die verwendeten ES6-JavaScript-Elemente vom Internet Explorer nicht unterstützt werden und dieser Browser damit ausgeschlossen ist.
Hinweis:
In diesem Artikel entsteht eine umfassende Neubearbeitung eines Selfhtml-aktuell-Artikels von Rodney Rehm aus dem Jahre 2006 mit dem Thema Verkettete Auswahllisten.

Struktur

HTML-Markup

Das Beispiel besteht aus einem Formular mit mehreren select-Menüs. Das erste ist bereits aktiv und mit Auswahlwerten versehen, die übrigen sind zunächst noch leer und mit dem disabled-Attribut ausgegraut.

Die Auswahlwerte des ersten select-Elements werden beim Abruf des Formulars durch den Server eingetragen. Auf diese Weise ist sichergestellt, dass das Formular auch dann nutzbar ist, wenn JavaScript nicht ausgeführt wird. Der Server muss dann beim Absenden des Formulars erkennen, dass die Auswahllisten für Vorlesung oder Termin leer sind, oder nicht zum Professor passen, die Optionen für diese Auswahllisten eintragen und das Formular erneut an den Browser senden (Affenformular).

Sobald JavaScript verfügbar ist, können wir die Abhängigkeiten zwischen den select Elementen besser unterstützen und die Auswahllisten der Folgestufen abhängig von der bisher getroffenen Auswahl füllen, ohne dafür das Formular an den Server senden zu müssen. Diese Vorgehensweise nennt man progressive Verbesserung (progressive enhancement). Auf die serverseitige Fallback-Lösung wird dieser Artikel nicht weiter eingehen.

verkettete Auswahllisten ansehen …
<form name="terminauswahl" method="post">
  <label>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ünz, Stefan</option>
      <option value="P27">Meier, Manfred</option>
    </select>
  </label>
  <label>Lesung:
    <select id="lesung" disabled><option value="">------</option></select>
  </label>
  <label>Termin:
    <select id="termin" disabled><option value="--">------</option></select>
  </label>
  <button type="submit">Absenden</button>
</form>

Struktur der Optionsdaten

Im Normalfall würden die Daten aus einer Datenbank vom Server kommen. 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.
  • Die Werte für jede Auswahlstufe befinden sich in einem Array. Jedes Arrayelement entspricht einer Option.
  • Ein solches Arrayelement enthält mindestens einen Schlüssel und einen Anzeigewert. Wenn das Element nicht zur letzten Auswahlstufe gehört, dann enthält es zusätzlich noch einen Eintrag mit einem untergeordneten Array, das die Werte für die nächste Auswahlstufe liefert
  • Bestimmte Voraussetzungen über die Namen der Eigenschaften, die in den Arrayelementen liegen, werden nicht getroffen. Das Herstellen einer Abbildung von den Arrayelementen auf die Auswahllisten wird Teil unseres Tutorials sein.

Auf der obersten Stufe stellen unsere Daten ein Array aus Professoren dar. Um nicht gleich zu unübersichtlich zu werden, sind die Vorlesungen erstmal durch "..." ersetzt.

const data = [
   { id: "P23", name: "Albers, Alfred", vorlesungen: [...] },
   { id: "P24", name: "Braining, Berta", vorlesungen: [...] },
   { id: "P25", name: "Drachenzaun, Doris", vorlesungen: [...] },
   ...
];

Jede Professorin und jeder Professor hält Vorlesungen. Diese werden als untergeordnetes Array in der vorlesungen-Eigenschaft gespeichert:

const data = [
   { id: "P23",
     name: "Albers, Alfred",
     vorlesungen: [
       { nummer: "L55", thema: "Katzen in der freien Wildbahn", termine: [...] },
       { nummer: "L56", thema: "Katzen im Gefängnis", termine: [...] },
       { nummer: "L57", thema: "Jellicle Cats", termine: [...] },
     ]
   }

Und schließlich gibt es für jede Vorlesung Termine. Auch diese sind wieder untergeordnete Arrays:

const data = [
   { id: "P23",
     name: "Albers, Alfred",
     vorlesungen: [
       ...,
       { nummer: "L57",
         thema: "Jellicle Cats",
         termine: [
           { id: "T127": zeit: "dienstags 10-12" },
           { id: "T128", zeit: "mittwochs 13-15" }
         ]
       },
     ]
   },
   ...
]

Die vollständige Initialisierung der data-Variablen finden Sie im Frickl, wenn Sie auf "ausprobieren" klicken. Ein serverseitiges Script kann ein solches JavaScript-Objekt entweder als JavaScript-Sourcecode generieren (was fehleranfällig ist), oder es baut eine passende Objektstruktur auf und erstellt daraus einen so genannten JSON String. Der Inhalt eines solchen Strings ist dem oben gezeigten Objektliteral ähnlich, und kann von JavaScript automatisch in ein Objekt überführt werden. Näheres dazu finden Sie in den Wiki-Kapiteln JSON und fetch. Da dem Self Wiki ein Beispielserver fehlt, bleibt es in diesem Tutorial bei einem Objektliteral, das Teil des Beispielcodes ist. Auf andere Arten der Datenbeschaffung wird am Ende, im Abschnitt möglichen Variationen, noch einmal eingegangen.

Programmstruktur

Unser Beispielprogramm wird aus drei wesentlichen Teilen bestehen. In einer realen Anwendung würde man diese Teile in eigenen .js Dateien speichern und als ECMAScript 6-Module gestalten.

Die Initialisierung

In einem DOMContentLoaded Handler werden die benötigten Objekte erzeugt. Sie werden untereinander, mit den HTML Elementen und mit dem Datenobjekt verknüpft. Die drei Klassen ProfessorController, VorlesungController und TerminController werden im folgenden noch vorgestellt. Ihre Aufgabe ist es, die Verbindung zwischen einer Datenquelle und einem select Element herzustellen, sowie über die Abhängigkeit zwischen den Auswahlebenen Bescheid zu wissen und über eine Veränderung der getroffenen Auswahl zu informieren. Durch diese Verlagerung der Arbeit auf dafür spezialisierte Klassen sieht die Initialisierung des Beispiels trügerisch einfach aus:

Initialisierung
document.addEventListener('DOMContentLoaded', function () {
	const profCtrl = new ProfessorController("professor");
	const vorlesungCtrl = new VorlesungController("vorlesung", profCtrl);
	const terminCtrl = new TerminController("termin", vorlesungCtrl);
	terminCtrl.addEventListener("change", terminChanged)

	profCtrl.mapData(data)
});

Die Controllerklassen erwarten jeweils die ID des Select-Elements, für das sie zuständig sein sollen. Controller, die ein abhängiges Select-Element steuern, bekommen den Controller dieses Elements als weiteren Parameter übergeben.

Auf dem TerminController wird noch ein Listener für ein change-Event registriert. Eine Behandlungsfunktion terminChanged kann dann die ausgewählten Daten auslesen und sie anzeigen. Vielleicht möchte man mit einem AJAX Request abfragen, ob an diesem Termin noch Plätze in der Vorlesung frei sind. Unser Beispiel wird die ausgewählten Werte lediglich in ein output-Element schreiben.

Als Ergebnis entsteht diese Objektstruktur:

Verkettete-auswahllisten-1.svg

Nachdem die Controller-Struktur aufgesetzt ist, wird dem ProfessorController, der die führende Auswahlliste steuert, die verwendete Datenquelle übergeben. Dazu dient die mapData-Methode.

Die Controller-Klassen

ProfessorController

Beginnen wir mit dem Controller für die Professor-Auswahl. Bei der Objekt-Erzeugung wurde lediglich die ID des select-Elements mitgegeben, für das der Controller zuständig sein soll.

Der ProfessorController
class ProfessorController extends SelectionController {
   constructor(selectElement) {
      super(selectElement, null);
   }
   getValue(quelle, profId) {
      return quelle.find(prof => prof.id == profId);  
   }
}

Das ist nun überraschend wenig. Es ist eine Klasse mit einer Konstruktorfunktion und einer einzigen Methode. Achten Sie auf die Angabe extends SelectionController: Der ProfessorController ist eine Subklasse von SelectionController und erbt damit dessen Methoden. Tatsächlich könnte man sogar ganz auf die ProfessorController Klasse verzichten, denn ihre Aufgabe ist lediglich, eine Hilfsmethode für den SelectionController bereitzustellen: getValue. Diese Methode wird vom SelectionController gebraucht, wenn er das Datenobjekt für einen ausgewählten Schlüssel benötigt, denn in den option Elementen des select stehen nur ein Schlüssel und ein Text, z.B. der "P23" und "Albers, Alfred", aber nicht das vollständige Objekt, das auch die Liste der Vorlesungen enthält.

Zur Implementierung von getValue wird auf die find-Methode von Array.prototype zurückgegriffen. Sie ruft pro Arrayelement den übergebenen Callback auf und liefert das erste Element, für das der Callback true zurückgibt.

Die Konstruktormethode des ProfessorController hat lediglich die Aufgabe, das übergebene selectElement an den Konstruktor des SelectionController weiterzuleiten. Dazu kommt noch ein weiterer Parameter null; was es damit auf sich hat, sehen Sie gleich.

Was tut der SelectionController nun damit? Schauen wir uns diese Klasse an.

SelectionController - Überblick

Auch der SelectionController ist keine autarke Klasse, sondern von einem im Browser implementierten Objekt abgeleitet: Dem EventTarget. EventTargets sind altbekannt, jedes HTML Element im DOM ist eines, aber dass man davon eigene Klassen ableiten kann ist ein relativ neues Feature in JavaScript. Damit ist es einer Klasse möglich, eigene Events anzubieten. Sie konnten das in der Initialisierung sehen, wo ein Handler für das change-Event des TerminController registriert wurde.

Es handelt sich hierbei um eine abstrakte Klasse. Darunter versteht man Klassen, die für einige oder alle Methoden keine Implementierung enthalten und deshalb nicht eigenständig funktionieren können. Sie dienen als Basis für konkrete Subklassen, die diese Methoden zuliefern. JavaScript kennt keine Syntax, um abstrakte Methoden zu deklarieren. Man könnte sie statt dessen als Dummy-Methoden implementieren, die ein Error-Objekt werfen.

Für eine der beiden abstrakten Methoden wird das auch gemacht. Die andere, getValueList, ist dagegen als optionale Methode vorgesehen. Dazu später mehr bei der Diskussion der mapData Methode.

Der SelectionController
// SelectionController ist eine abstrakte Basisklasse für die konkreten Controller, die 
// zur Anbindung der select Elemente dienen
class SelectionController extends EventTarget {
	constructor(selectElement, parentNode) {
		super();		// Pflicht: Konstruktor der Superklasse aufrufen
		if (!(selectElement instanceof HTMLSelectElement)) {
			throw new Error("Controller-Objekt benötigt ein select Element als ersten Parameter");
		}
		if (parentNode && !(parentNode instanceof SelectionController)) {
			throw new Error("Controller-Objekt benötigt einen SelectionController als zweiten Parameter");
		}
		this.selectElement = selectElement;
		this.parentNode = parentNode;

		this.selectElement.addEventListener("change", event => this._handleChangeEvent(event))
		if (parentNode) {
			parentNode.addEventListener("change", event => this.mapData(event.selectedObject));
		}
	}

	// Ordnet dem select Element eine Datenquelle zu. 
	mapData(dataSource) {
		// Das kriegen wir später...
	}

	// Abstrakte Methode! Wird sie nicht überschrieben, wird der TypeError geworfen
	getValue(key) {
		throw new TypeError("Die abstrakte Methode 'getValue' wurde nicht implementiert!");
	}

	// Stellt den im select Element ausgewählten Optionswert zur Verfügung
	get selectedKey() {
		return this.selectElement.value;
	}

	// Liefert das Datenobjekt zum ausgewählten Optionswert
	get selectedObject() {
		return this.dataSource ? this.getValue(this.dataSource, this.selectedKey) : null;
	}

	// privat
	// Die Methode reagiert auf das change-Event des select-Elements
	// und stellt es als eigenes change-Event des Controllers zur Verfügung
	_handleChangeEvent(event) {
		let nodeChangeEvent = new Event("change");
		nodeChangeEvent.selectedValue = this.selectedObject;
		this.dispatchEvent(nodeChangeEvent);
	}
}

Außer dem Aufruf des super-Konstruktors tut der Konstruktor nicht viel. Er prüft, ob er die erwarteten Datentypen als Argumente erhalten hat, er ein selectElement bekommen hat, merkt sich die übergebenen Argumente und registriert auf dem übergebenen select-Element einen change-Handler, um auf Benutzereingaben registrieren zu können. Hinzu kommt noch eine Registrierung für das change-Event des übergeordneten Controllers - sofern einer vorhanden ist. Auf diesem Weg bemerkt eine untergeordnete Auswahlliste, dass sich die übergeordnete Auswahl verändert hat, und kann die angebotenen Werte aktualisieren.

Genau das tut der Eventhandler, der im Konstruktor als einfache Pfeilfunktion hinterlegt wird. Er nimmt den ausgewählten Wert des Elternelements und ruft damit die eigene mapData Methode auf. An dieser Stelle sehen wir den Sinn der getValue Methode, die von der abgeleiteten ProfessorController Klasse bereitgestellt wird. Ein SelectionController ist abstrakt, er weiß nichts über die dargestellten Daten. Diese Aufgabe übernimmt die Subklasse. Den Aufruf von getValue finden wir in der getter-Methode selectedObject. Diese prüft, ob überhaupt Daten zugeordnet wurden (das wird mapData tun). Wenn ja, ruft sie getValue mit diesen Daten und dem im select ausgewählten Wert auf.

Eigentlich müsste der Konstruktor noch die Parameter auf Plausibilität prüfen. Steht in selectElement ein String? Existiert ein DOM Element mit dieser ID? Ist das auch ein select Element? Ist der übergebene parentNode ein SelectionController? Solche Plausibilitätsprüfungen machen Code sehr schnell sehr unübersichtlich, darum wurden sie hier weggelassen.

Ansonsten bietet der SelectionController noch zwei Eigenschafts-Getter an, das sind Methoden, die wie eine schreibgeschützte Eigenschaft verwendet werden. Mit selectedKey und selectedObject stellt der SelectionController den ausgewählten Schlüssel und das zugehörige Datenobjekt zur Verfügung.

Die private Methode _handleChangeEvent verwendet das geerbte EventTarget, um ein change Event des select Elements weiterzumelden. Eigentlich könnte sie privat sein, aber JavaScript unterstützt noch keine privaten Elemente in Klassen, daher wird nur die Unterstrich-Schreibweise benutzt, um die Privatheit der Methode zu symbolisieren. Hier wird ein neues Event vom Typ "change" erzeugt und um eine Eigenschaft selectedValue erweitert. Wenn Sie im Konstruktor auf die Registrierung des change-Events beim parentNode schauen, dann sehen Sie, dass selectedValue dort verwendet wird.

Ein solches Muster findet man in gekoppelten Systemen häufig. Untergeordnete Objekte kennen ihr übergeordnetes Objekt und können über eine parent-Eigenschaft darauf zugreifen, aber andersherum weiß das übergeordnete Objekt nicht, wer ihm alles untergeordnet ist und kann sich nur durch Events bemerkbar machen. Auf diese Weise könnten dem Professor-select auch mehr als eine direkt untergeordnete Auswahlliste zugeordnet sein.

SelectionController - data mapping

Kommen wir nun zur Magie des Ganzen: die mapData Methode des SelectionControllers.

Sie erwartet als Parameter ein Array mit Werten, die vom select Element als Optionen angeboten werden sollen. Dieses Array speichert sie zur weiteren Verwendung in der Eigenschaft dataSource des SelectionController ab. Der weitere Code der Methode besteht im Wesentlichen aus einer Steuerlogik, die für die eigentliche Arbeit weitere Funktionen aufruft. Deren Namen sind so sprechend gewählt, dass der Code selbsterklärend wird.

Der SelectionController
	// Ordnet dem select Element eine Datenquelle zu. 
	// dataSource ist ein Objekt, aus dem die getValueList die Daten für die
	// select-Optionen ermitteln kann, oder null, um das select-Element zu
	// disablen
	mapData(dataSource) {
		// Quelldaten-Objekt im Controller speichern.
		this.dataSource = dataSource;

		// Optionen nur anfassen, wenn eine getValueList Methode vorhanden ist.
		//  Andernfalls davon ausgehen, dass die options durch das HTML
		// bereitgestellt werden.
		if (typeof this.getValueList == "function") {

			// Existierende Optionen entfernen
			removeOptions(this.selectElement);

			// Neue Optionen aus der Datenquelle beschaffen - sofern sie exisi
			const options = dataSource && this.getValueList(dataSource);
			if (!options|| !options.length) {
				setToDisabled(this.selectElement);
			} else {
				setToEnabled(this.selectElement, options);
			}
		}
		this.selectElement.dispatchEvent(new Event("change"));

		// Helper: Entferne alle options aus einem select Element	
		function removeOptions(selectElement) {
			while(this.selectElement.length > 0)
			this.selectElement.remove(0);
		}

		// Helper: select-Element auf disabled setzen und eine Dummy-Option 
		// eintragen. Eine Variante wäre: das selectElement auf hidden setzten
		function setToDisabled(selectElement) {
			addOption(selectElement, "", "------");
			selectElement.disabled = true;
		}

		// Helper: disabled-Zustand vom select-Element entfernen und die
		// übergebenen Optionen eintragen. Vorweg eine Dummy-Option "Bitte wählen".
		function setToEnabled(selectElement, options) {
			addOption(selectElement, "", "Bitte wählen:");

			for (var optionData of options) {
				addOption(selectElement, optionData.value, optionData.text);
			}
			selectElement.disabled = false;
		}
    
		// Helper: Option-Element erzeugen, ausfüllen und im select-Element eintragen
		function addOption(selectElement, value, text) {
			let option = document.createElement("option");
			option.value = value;
			option.text = text
			selectElement.add(option);
		}
	}

Als nächstes prüft sie das Vorhandensein einer Methode, die wir bisher noch nicht gesehen haben: getValueList. Hierbei handelt es sich um die zuvor schon erwähnte optionale Methode. Ist sie nicht vorhanden, geht mapData davon aus, das die Optionsliste im HTML vorhanden ist und unverändert bleibt. Es wird auch davon ausgegangen, dass dieses select-Element niemals disabled wird. Das ist bei der Professoren-Auswahl der Fall, weshalb es im ProfessorController auch keine getValueList Methode gibt. In diesem Fall wird nur noch auf dem select-Element das change-Event ausgelöst. Das hat Konsequenzen, denn nun wird die _handleChangeEvent Methode des SelectionController aktiv und benachrichtigt die nachfolgende Auswahlliste, dass sie sich aktualisieren soll - was auch dort zu einem mapData Aufruf führt.

Der mapData-Aufruf, der zu Anfang in der Initialisierungsroutine ausgeführt wurde, wird auf diese Weise an die ganze Kette der abhängigen Auswahllisten durchgereicht.

Anders ist es, wenn die getValueList Methode vorhanden ist. mapData verlangt von ihr, dass sie ein Array aus Objekten liefert. Jedes dieser Objekte muss zwei Eigenschaften enthalten: value und text. Aus diesen Objekten baut mapData die Optionenliste des select Elements auf. Der SelectionController ist eine abstrakte Klasse, das bedeutet: er implementiert getValueList nicht selbst, sondern erwartet, dass sie von einer abgeleiteten Klasse bereitgestellt wird. JavaScript kennt leider kein Sprachmittel, um diesen Sachverhalt darzustellen.

Zuerst allerdings werden die existierenden Optionen entfernt. Über eine while-Schleife wird solange die erste Option entfernt, bis keine mehr da ist.

Nun werden die anzuzeigenden Werte mit getValueList beschafft, sofern eine Datenquelle übergeben wurde. Wenn nicht, oder wenn getValueList nichts liefert, steht in werte null oder ein leeres Array.

Dieser Sonderfall einer leere Datenquelle wird nun zuerst behandelt. Das kann passieren, wenn im übergeordneten Element kein gültiger Schlüssel ausgewählt ist, also das "Bitte wählen" Element oder das "-----" Element. Bei einer leeren Datenquelle bekommt das select Element lediglich einen Dummy-Eintrag mit einem Leerstring als Optionswert und "-----" als Anzeigetext, danach wird das Element gesperrt, das change-Event ausgelöst und mapData beendet. Das change-Event ist wichtig, damit alle nachfolgenden Auswahlelemente ebenfalls aktualisiert werden.

Das eigentliche Eintragen einer Option erfolgt mit einer Helfer-Funktion addOption, die die niedrigen Tätigkeiten beim Erzeugen und Anfügen eines HTMLOptionElement-Objekts erledigt.

Wenn Werte gefunden wurden, kann das select Element damit befüllt werden. Als erstes kommt ein "Bitte wählen:" Eintrag in die Liste, und danach wird mit Hilfe der foreach Methode einmal pro Eintrag im werte-Array addOption aufgerufen. An solchen Stellen erkennt man den besonderen Nutzen von Pfeilfunktionen. Würde man statt dessen eine anonyme Funktion verwenden, wäre es ohne eine Hilfsvariable nicht möglich, die selectElement Eigenschaft von this einfach im Callback zu verwenden.

Zum Abschluss wird das select-Element vom disabled-Zustand befreit und einmal das change-Event darauf ausgelöst, um abhängige Auswahllisten zu benachrichtigen

Zum Abschluss: VorlesungController und TerminController

Diese beiden Klassen haben nun noch die Aufgabe, getValueList und getValue passend zu der in data abgelegten Arraystruktur bereitzustellen. Sie sehen so aus:

VorlesungController und TerminController
class VorlesungController extends SelectionController {
   constructor(selectElement, parentController) {
      super(selectElement, parentController);
   }
   getValueList(professor) {
     return professor.vorlesungen.map(vorlesung=> ({ value: vorlesung.nummer, text: vorlesung.thema }));
   }
   getValue(professor, vorlesungNr) {
      return professor.vorlesungen.find(vorlesung => vorlesung.nummer == vorlesungNr); 
   }
}

class TerminController extends SelectionController {
   constructor(selectElement, parentController) {
      super(selectElement, parentController);
   }
   getValueList(vorlesung) {
     return vorlesung.termine.map(termin => ({ value: termin.id, text: termin.zeit }));
   }
   getValue(vorlesung, terminId) {
      return vorlesung.termine.find(termin => termin.id == terminId); 
   }
}

Die Konstruktoren reichen lediglich ihre Parameter durch. Leider kann man Konstruktoren in JavaScript nicht vererben, ansonsten wäre dieser Umstand überflüssig. Die getValue Methoden funktionieren analog zu der im ProfessorController und benötigen wohl keine weitere Erläuterung.

Die getValueList Methode ist für diese beiden Klassen neu. Diese Methode wird von mapData aufgerufen und bekommt das ausgewählte Objekt der übergeordneten Auswahlliste übergeben. Sie hat die Aufgabe, daraus die Werte für die aktuelle Auswahlliste zu extrahieren. Im Fall des VorlesungController finden sich die Vorlesungen als Array in der vorlesungen Eigenschaft des Professor-Objekts. Mit Hilfe der map Methode wird dieses Array in die gewünschte Form transformiert. map ruft den angegebenen Callback einmal für jedes Arrayelement auf und erzeugt aus den Rückgabewerten ein neues Array.

Als Callback wird wieder einmal eine Pfeilfunktion verwendet. Beabsichtigt ist, dass sie mittels eines Objektliterals ein Objekt zurückgibt. Hier kommt eine Syntaxbesonderheit von JavaScript ins Spiel: Wenn man direkt nach dem Pfeil eine linke geschweifte Klammer setzt, deutet JavaScript das nicht als Beginn eines Objektliterals, sondern als Beginn eines Anweisungsblocks. Deshalb müssen zunächst runde Klammern gesetzt werden, damit JavaScript weiß, dass kein Anweisungsblock folgt, sondern ein einfacher Ausdruck, dessen Wert zurückgegeben werden soll.

Mögliche Variationen

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

Um die Vorteile von AjaX nutzen zu können, muss die getValueList-Methode der Controller angepasst werden. An Stelle eines Arrays könnte die Eigenschaft vorlesungen bzw. termine eine URL enthalten, die für den Datenabruf verwendet werden kann. Es ist auch eine Modifikation im SelectionController notwendig, um das asynchrone Ergebnis des AjaX-Aufrufs in die Verarbeitung von mapData einspeisen zu können. Man könnte es zum Beispiel so bauen, dass getValueList ein Promise zurückliefern muss, das die Werte enthält, und mapData seine Arbeit über .then an das Promise anschließt. Ein Zwischenspeichern der AjaX-Ergebnisse innerhalb der Anwendung könnte man vorsehen, aber eigentlich ist es besser, wenn der Server seine Ergebnisse mit einem passenden Cache-Control Header liefert und so das eingebaute Caching des Browsers genutzt werden kann.

Datenformatierung bei AJAX

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 this.selectElement.innerHTML eingefügt wird.

Auswahllisten-Objekte dynamisch erstellen

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. Man müsste sich dann auch ein Verfahren überlegen, wie man die erforderlichen Controller-Objekte bei Bedarf erzeugt und ankoppelt.

Auswahllisten dynamisch erscheinen lassen

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.