Abhängige Selectboxen elegant in Nette und purem JS
Wie kann man verkettete Auswahlboxen erstellen, bei denen nach der Auswahl eines Wertes in der einen die Optionen in der anderen dynamisch aktualisiert werden? Das ist eine einfache Aufgabe in Nette und purem JavaScript. Wir zeigen eine Lösung, die sauber, wiederverwendbar und sicher ist.
Datenmodell
Lassen Sie uns als Beispiel ein Formular erstellen, das Auswahlfelder für die Auswahl des Landes und der Stadt enthält.
Zunächst bereiten wir ein Datenmodell vor, das Einträge für beide Auswahlfelder zurückgibt. Wahrscheinlich wird es diese aus der Datenbank abrufen. Die genaue Implementierung ist nicht unbedingt erforderlich, also lassen Sie uns nur andeuten, wie die Schnittstelle aussehen wird:
class World
{
public function getCountries(): array
{
return ...
}
public function getCities($country): array
{
return ...
}
}
Da die Gesamtzahl der Städte sehr groß ist, werden wir sie über AJAX
abrufen. Zu diesem Zweck werden wir eine EndpointPresenter
erstellen, eine API, die die Städte in jedem Land als JSON zurückgibt:
class EndpointPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private World $world,
) {}
public function actionCities($country): void
{
$cities = $this->world->getCities($country);
$this->sendJson($cities);
}
}
Wenn es nur wenige Städte gibt (z. B. auf einem anderen Planeten 😉 ) oder
wenn das Modell Daten darstellt, die einfach nicht viele sind, könnten wir sie
alle als Array an JavaScript übergeben und AJAX-Anfragen sparen. In diesem Fall
besteht keine Notwendigkeit für EndpointPresenter
.
Formular
Kommen wir nun zum Formular selbst. Wir werden zwei Auswahlfelder erstellen
und sie miteinander verknüpfen, d. h. wir werden die untergeordneten Elemente
(city
) in Abhängigkeit vom ausgewählten Wert des übergeordneten
Elements (country
) setzen. Wichtig ist, dass wir dies im onAnchor-Ereignishandler
tun, d. h. in dem Moment, in dem das Formular bereits die vom Benutzer
eingegebenen Werte kennt.
class DemoPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private World $world,
) {}
protected function createComponentForm(): Form
{
$form = new Form;
$country = $form->addSelect('country', 'Country:', $this->world->getCountries())
->setPrompt('----');
$city = $form->addSelect('city', 'City:');
// <-- Wir fügen hier noch etwas hinzu
$form->onAnchor[] = fn() =>
$city->setItems($country->getValue()
? $this->world->getCities($country->getValue())
: []);
// $form->onSuccess[] = ...
return $form;
}
}
Das auf diese Weise erstellte Formular funktioniert ohne JavaScript. Dies geschieht, indem der Benutzer zunächst ein Land auswählt, das Formular abschickt, dann ein Menü mit Städten erscheint, eine davon auswählt und das Formular erneut abschickt.
Wir sind jedoch daran interessiert, die Städte mit Hilfe von JavaScript
dynamisch zu laden. Der sauberste Weg, dies zu tun, ist die Verwendung von
data-
-Attributen, mit denen wir dem HTML-Code (und damit JS)
Informationen darüber übermitteln, welche Auswahlfelder verknüpft sind und
woher die Daten abgerufen werden sollen.
Für jedes untergeordnete Auswahlfeld übergeben wir ein
data-depends
-Attribut mit dem Namen des übergeordneten Elements
und dann entweder ein data-url
-Attribut mit der URL, von der aus
die Elemente über AJAX abgerufen werden, oder ein data-items
-Attribut, in dem wir alle Optionen direkt auflisten.
Beginnen wir mit der AJAX-Variante. Wir übergeben den Namen des
übergeordneten Elements country
und einen Verweis auf
Endpoint:cities
. Wir verwenden das Zeichen “#” als Platzhalter
und JavaScript setzt stattdessen den vom Benutzer ausgewählten
Schlüssel ein.
$city = $form->addSelect('city', 'City:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-url', $this->link('Endpoint:cities', '#'));
Und die Variante ohne AJAX? Wir bereiten ein Array mit allen Ländern und
allen Städten vor, das wir an das Attribut data-items
übergeben:
$items = [];
foreach ($this->world->getCountries() as $id => $name) {
$items[$id] = $this->world->getCities($id);
}
$city = $form->addSelect('city', 'City:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-items', $items);
JavaScript-Handler
Der folgende Code ist universell, er ist nicht an die spezifischen
Auswahlfelder country
und city
aus dem Beispiel
gebunden, sondern verknüpft alle Auswahlfelder auf der Seite, indem er einfach
die erwähnten data-
Attribute setzt.
Der Code ist in reinem Vanilla JS geschrieben, benötigt also weder jQuery noch eine andere Bibliothek.
// alle untergeordneten Selectboxen auf der Seite finden
document.querySelectorAll('select[data-depends]').forEach((childSelect) => {
let parentSelect = childSelect.form[childSelect.dataset.depends]; // übergeordnetes <select>
let url = childSelect.dataset.url; // Attribut data-url
let items = JSON.parse(childSelect.dataset.items || 'null'); // Attribut data-items
// wenn der Benutzer das ausgewählte Element in der übergeordneten Auswahl ändert...
parentSelect.addEventListener('change', () => {
// wenn das data-items-Attribut existiert...
if (items) {
// neue Elemente direkt in die untergeordnete Auswahlbox laden
updateSelectbox(childSelect, items[parentSelect.value]);
}
// wenn das data-url-Attribut existiert...
if (url) {
// stellen wir eine AJAX-Anfrage an den Endpunkt mit dem ausgewählten Element anstelle des Platzhalters
fetch(url.replace(encodeURIComponent('#'), encodeURIComponent(parentSelect.value)))
.then((response) => response.json())
// und laden neue Elemente in die Child-Selectbox
.then((data) => updateSelectbox(childSelect, data));
}
});
});
// Ersetzt <options> in <select>
function updateSelectbox(select, items)
{
select.innerHTML = ''; // alle entfernen
for (let id in items) { // neu einfügen
let el = document.createElement('option');
el.setAttribute('value', id);
el.innerText = items[id];
select.appendChild(el);
}
}
Mehr Elemente und Wiederverwendbarkeit
Die Lösung ist nicht auf zwei Auswahlfelder beschränkt, Sie können eine Kaskade von drei oder mehr abhängigen Elementen erstellen. Zum Beispiel fügen wir eine Straßenauswahl hinzu, die von der ausgewählten Stadt abhängt:
$street = $form->addSelect('street', 'Ulice:')
->setHtmlAttribute('data-depends', $city->getHtmlName())
->setHtmlAttribute('data-url', $this->link('Endpoint:streets', '#'));
$form->onAnchor[] = fn() =>
$street->setItems($city->getValue() ? $this->world->getStreets($city->getValue()) : []);
Auch mehrere Auswahlfelder können von einem einzigen gemeinsamen abhängen.
Setzen Sie einfach die data-
Attribute analog und füllen Sie die
Elemente mit setItems()
.
Es ist absolut nicht notwendig, den JavaScript-Code zu ändern, der universell funktioniert.
Sicherheit
Auch in diesen Beispielen bleiben alle Sicherheitsmechanismen, die Nette-Formulare haben, erhalten. Insbesondere wird bei jedem Auswahlfeld geprüft, ob die gewählte Option eine der angebotenen ist, so dass ein Angreifer keinen anderen Wert fälschen kann.
Die Lösung funktioniert in Nette 2.4 und höher, die Codebeispiele sind
für PHP 8 geschrieben. Damit sie in älteren Versionen funktionieren, ersetzen
Sie property
promotion und fn()
durch
function () use (...) { ... }
.
Um einen Kommentar abzugeben, loggen Sie sich bitte ein