Selectbox-uri dependente elegant în Nette și JavaScript pur
Cum să creăm selectbox-uri interconectate, unde după selectarea unei valori într-unul, opțiunile se încarcă dinamic în celălalt? În Nette și JavaScript pur, este o sarcină ușoară. Vom arăta o soluție curată, reutilizabilă și sigură.

Model de date
Ca exemplu, vom crea un formular care conține selectbox-uri pentru alegerea țării și a orașului.
Mai întâi, vom pregăti modelul de date care va returna elementele pentru ambele selectbox-uri. Probabil le va obține dintr-o bază de date. Implementarea exactă nu este esențială, așa că vom indica doar cum va arăta interfața:
class World
{
public function getCountries(): array
{
return ...
}
public function getCities($country): array
{
return ...
}
}
Deoarece numărul total de orașe este foarte mare, le vom obține folosind
AJAX. În acest scop, vom crea un EndpointPresenter
, adică un API
care ne va returna orașele din fiecare țară în format JSON:
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);
}
}
Dacă ar fi puține orașe (poate pe altă planetă 😉), sau dacă modelul
ar reprezenta date care pur și simplu nu sunt multe, am putea să le transmitem
pe toate direct ca un array în JavaScript și să economisim cererile AJAX. În
acest caz, EndpointPresenter
nu ar fi necesar.
Formular
Și să trecem la formularul în sine. Vom crea două selectbox-uri și le
vom interconecta, adică vom seta elementele pentru cel subordonat
(city
) în funcție de valoarea selectată în cel superior
(country
). Important este că facem acest lucru în handler-ul
evenimentului onAnchor
,
adică în momentul în care formularul cunoaște deja valorile trimise de
utilizator.
class DemoPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private World $world,
) {}
protected function createComponentForm(): Form
{
$form = new Form;
$country = $form->addSelect('country', 'Țară:', $this->world->getCountries())
->setPrompt('----');
$city = $form->addSelect('city', 'Oraș:');
// <-- aici vom mai completa ceva
$form->onAnchor[] = fn() =>
$city->setItems($country->getValue()
? $this->world->getCities($country->getValue())
: []);
// $form->onSuccess[] = ...
return $form;
}
}
Formularul creat astfel va funcționa și fără JavaScript. Și anume, utilizatorul selectează mai întâi țara, trimite formularul, apoi apare oferta de orașe, selectează unul dintre ele și trimite formularul din nou.
Dar pe noi ne interesează încărcarea dinamică a orașelor folosind
JavaScript. Cel mai curat mod de a aborda acest lucru este să utilizăm
atribute data-
, în care vom trimite informații către HTML (și,
prin urmare, JS) despre care selectbox-uri sunt interconectate și de unde
trebuie să se obțină datele.
Fiecărui selectbox subordonat îi vom transmite atributul
data-depends
cu numele elementului superior și, în plus, fie
data-url
cu URL-ul de unde trebuie să obțină elementele folosind
AJAX, fie data-items
, unde vom lista direct toate variantele.
Să începem cu varianta AJAX. Vom transmite numele elementului superior
country
și linkul către Endpoint:cities
. Folosim
caracterul #
ca substituent (placeholder), iar JavaScript va insera
în locul lui cheia selectată de utilizator.
$city = $form->addSelect('city', 'Oraș:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-url', $this->link('Endpoint:cities', '#'));
Și varianta fără AJAX? Vom pregăti un array cu toate țările și toate
orașele lor, pe care îl vom transmite atributului data-items
:
$items = [];
foreach ($this->world->getCountries() as $id => $name) {
$items[$id] = $this->world->getCities($id);
}
$city = $form->addSelect('city', 'Oraș:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-items', $items);
Și rămâne să scriem handler-ul JavaScript.
Handler JavaScript
Următorul cod este universal, nu este legat de selectbox-urile specifice
country
și city
din exemplu, ci va interconecta orice
selectbox-uri de pe pagină, este suficient doar să le setați atributele
data-
menționate.
Codul este scris în JavaScript pur (vanilla JS), deci nu necesită jQuery sau altă bibliotecă.
// găsim pe pagină toate selectbox-urile subordonate
document.querySelectorAll('select[data-depends]').forEach((childSelect) => {
let parentSelect = childSelect.form[childSelect.dataset.depends]; // <select> superior
let url = childSelect.dataset.url; // atributul data-url
let items = JSON.parse(childSelect.dataset.items || 'null'); // atributul data-items
// când utilizatorul schimbă elementul selectat în selectul superior...
parentSelect.addEventListener('change', () => {
// dacă există atributul data-items...
if (items) {
// încărcăm direct în selectbox-ul subordonat elementele noi
updateSelectbox(childSelect, items[parentSelect.value]);
}
// dacă există atributul data-url...
if (url) {
// facem o cerere AJAX către endpoint cu elementul selectat în locul substituentului
fetch(url.replace(encodeURIComponent('#'), encodeURIComponent(parentSelect.value)))
.then((response) => response.json())
// și încărcăm în selectbox-ul subordonat elementele noi
.then((data) => updateSelectbox(childSelect, data));
}
});
});
// suprascrie <options> în <select>
function updateSelectbox(select, items)
{
select.innerHTML = ''; // eliminăm tot
for (let id in items) { // inserăm elementele noi
let el = document.createElement('option');
el.setAttribute('value', id);
el.innerText = items[id];
select.appendChild(el);
}
}
Mai multe elemente și reutilizabilitate
Soluția nu este limitată la două selectbox-uri, se poate crea chiar și o cascadă de trei sau mai multe elemente dependente între ele. De exemplu, vom adăuga alegerea străzii, care va depinde de orașul selectat:
$street = $form->addSelect('street', 'Stradă:')
->setHtmlAttribute('data-depends', $city->getHtmlName())
->setHtmlAttribute('data-url', $this->link('Endpoint:streets', '#'));
$form->onAnchor[] = fn() =>
$street->setItems($city->getValue() ? $this->world->getStreets($city->getValue()) : []);
De asemenea, mai multe selectbox-uri pot depinde de unul comun. Este
suficient doar să setați analogic atributele data-
și să
completați elementele folosind setItems()
.
În același timp, nu este necesară nicio intervenție în codul JavaScript, care funcționează universal.
Securitate
Chiar și în aceste exemple, se păstrează în continuare toate mecanismele de securitate de care dispun formularele în Nette. În special, fiecare selectbox verifică dacă varianta selectată este una dintre cele oferite și, prin urmare, un atacator nu poate introduce o altă valoare.
Soluția funcționează în Nette 2.4 și versiuni mai noi, exemplele de
cod sunt scrise pentru Nette pentru PHP 8. Pentru a funcționa în versiuni mai
vechi, înlocuiți property
promotion și fn()
cu
function () use (...) { ... }
.
Pentru a trimite un comentariu, vă rugăm să vă conectați