Odvisna izbirna polja elegantno v Nette in čistem JS
Kako ustvariti verižna izbirna polja, kjer se po izbiri vrednosti v enem dinamično posodobijo možnosti v drugem? To je enostavno opravilo v programu Nette in čistem javascriptu. Prikazali bomo rešitev, ki je čista, ponovno uporabna in varna.
Podatkovni model
Kot primer ustvarimo obrazec z izbirnimi polji za izbiro države in mesta.
Najprej bomo pripravili podatkovni model, ki bo vrnil vnose za obe izbirni polji. Verjetno jih bo pridobil iz podatkovne zbirke. Natančna implementacija ni bistvena, zato samo namignimo, kako bo videti vmesnik:
class World
{
public function getCountries(): array
{
return ...
}
public function getCities($country): array
{
return ...
}
}
Ker je skupno število mest zelo veliko, jih bomo pridobili z uporabo
AJAX-a. V ta namen bomo ustvarili EndpointPresenter
, API, ki bo
vrnil mesta v vsaki državi v obliki 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);
}
}
Če je mest malo (na primer na drugem planetu 😉) ali če model predstavlja
podatke, ki jih preprosto ni veliko, jih lahko vsa posredujemo kot polje
v JavaScript in prihranimo zahteve AJAX. V tem primeru ne bi bilo potrebe po
EndpointPresenter
.
Obrazec
Preidimo na sam obrazec. Ustvarili bomo dve izbirni polji in ju povezali, kar
pomeni, da bomo otroške (city
) elemente nastavili glede na izbrano
vrednost starševskega (country
). Pomembno je, da to storimo
v izvajalcu dogodka onAnchor,
tj. v trenutku, ko obrazec že pozna vrednosti, ki jih je posredoval
uporabnik.
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:');
// <-- tukaj bomo dodali še kaj drugega
$form->onAnchor[] = fn() =>
$city->setItems($country->getValue()
? $this->world->getCities($country->getValue())
: []);
// $form->onSuccess[] = ...
return $form;
}
}
Tako ustvarjen obrazec bo deloval brez uporabe JavaScripta. To naredimo tako, da uporabnik najprej izbere državo, odda obrazec, nato se mu prikaže meni z mesti, izbere eno od njih in znova odda obrazec.
Vendar nas zanima dinamično nalaganje mest z uporabo JavaScripta.
Najčistejši način za to je uporaba atributov data-
, s katerimi
pošljemo HTML (in s tem JS) informacije o tem, katera izbirna polja so
povezana in od kod je treba pridobiti podatke.
Za vsako podrejeno izbirno polje posredujemo atribut
data-depends
z imenom nadrejenega elementa, nato pa bodisi atribut
data-url
z naslovom URL, od koder se elementi pridobijo z uporabo
AJAX-a, bodisi atribut data-items
, v katerem neposredno navedemo
vse možnosti.
Začnimo z različico AJAX. Predamo ime nadrejenega elementa
country
in sklic na Endpoint:cities
. Znak
#
uporabimo kot nadomestek, JavaScript pa bo namesto njega vstavil
ključ, ki ga je izbral uporabnik.
$city = $form->addSelect('city', 'City:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-url', $this->link('Endpoint:cities', '#'));
In varianta brez AJAX-a? Pripravimo polje vseh držav in vseh njenih mest, ki
ga posredujemo atributu data-items
:
$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);
Obvladovalnik JavaScript
Naslednja koda je univerzalna, ni vezana na posebna izbirna polja
country
in city
iz primera, temveč bo povezala vsa
izbirna polja na strani, samo nastavite omenjene atribute
data-
.
Koda je napisana v čistem vanilla JS, zato ne potrebuje jQueryja ali katere koli druge knjižnice.
// poiščite vsa otroška izbirna polja na strani.
document.querySelectorAll('select[data-depends]').forEach((childSelect) => {
let parentSelect = childSelect.form[childSelect.dataset.depends]; // nadrejeni <select>
let url = childSelect.dataset.url; // atribut data-url
let items = JSON.parse(childSelect.dataset.items || 'null'); // atribut data-items
// ko uporabnik spremeni izbrani element v nadrejenem izbirnem polju...
parentSelect.addEventListener('change', () => {
// če atribut data-items obstaja...
if (items) {
// naloži nove elemente neposredno v podrejeno izbirno polje
updateSelectbox(childSelect, items[parentSelect.value]);
}
// če obstaja atribut data-url...
if (url) {
// naredimo zahtevo AJAX do končne točke z izbranim elementom namesto nadomestnega elementa
fetch(url.replace(encodeURIComponent('#'), encodeURIComponent(parentSelect.value)))
.then((response) => response.json())
// in naložimo nove elemente v otroško izbirno polje
.then((data) => updateSelectbox(childSelect, data));
}
});
});
// nadomesti <options> v <select>
function updateSelectbox(select, items)
{
select.innerHTML = ''; // odstrani vse
for (let id in items) { // vstavi nove
let el = document.createElement('option');
el.setAttribute('value', id);
el.innerText = items[id];
select.appendChild(el);
}
}
Več elementov in ponovna uporabnost
Rešitev ni omejena na dve izbirni polji, temveč lahko ustvarite kaskado treh ali več odvisnih elementov. Na primer, dodamo izbiro ulice, ki je odvisna od izbranega mesta:
$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()) : []);
Tudi več izbirnih polj je lahko odvisnih od enega skupnega. Preprosto
analogno nastavite atribute data-
in napolnite elemente z
setItems()
.
Pri tem nikakor ni treba spreminjati kode JavaScript, ki deluje univerzalno.
Varnost
Tudi v teh primerih so ohranjeni vsi varnostni mehanizmi, ki jih imajo obrazci Nette. Zlasti vsako polje za izbiro preveri, ali je izbrana možnost ena od ponujenih, zato napadalec ne more ponarediti drugačne vrednosti.
Rešitev deluje v sistemu Nette 2.4 in novejših, primeri kode so
napisani za PHP 8. Če želite, da delujejo v starejših različicah,
zamenjajte property
promotion in fn()
s
function () use (...) { ... }
.
Če želite oddati komentar, se prijavite