Függő selectboxok elegánsan Nette és tiszta JS-ben
Hogyan hozhatok létre láncolt selectboxokat, ahol az egyikben egy érték kiválasztása után az opciók dinamikusan frissülnek a másikban? Ez egyszerű feladat a Nette-ben és a pure JavaScriptben. Olyan megoldást mutatunk, amely tiszta, újrafelhasználható és biztonságos.
Adatmodell
Példaként hozzunk létre egy űrlapot, amely az ország és a város kiválasztására szolgáló kiválasztó dobozokat tartalmaz.
Először is készítsünk egy adatmodellt, amely mindkét kiválasztási mezőre vonatkozó bejegyzéseket ad vissza. Valószínűleg az adatbázisból fogja azokat lekérni. A pontos megvalósítás nem lényeges, ezért csak utalunk arra, hogy hogyan fog kinézni a felület:
class World
{
public function getCountries(): array
{
return ...
}
public function getCities($country): array
{
return ...
}
}
Mivel a városok száma nagyon nagy, AJAX segítségével fogjuk lekérni
őket. Ehhez létrehozunk egy EndpointPresenter
, egy API-t, amely
JSON formájában adja vissza az egyes országok városait:
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);
}
}
Ha kevés város van (például egy másik bolygón 😉 ), vagy ha a modell
olyan adatokat képvisel, amelyek egyszerűen nem sokak, akkor az összeset
tömbként átadhatjuk a JavaScriptnek, és megspórolhatjuk az
AJAX-kéréseket. Ebben az esetben nem lenne szükség a
EndpointPresenter
.
Form
És térjünk rá magára az űrlapra. Létrehozunk két kiválasztó dobozt,
és összekapcsoljuk őket, azaz a szülő (country
) kiválasztott
értékétől függően állítjuk be a gyermek (city
) elemeket.
A lényeg, hogy ezt az onAnchor
eseménykezelőben tesszük, vagyis abban a pillanatban, amikor az űrlap már
ismeri a felhasználó által megadott értékeket.
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:');
// <-- még valami mást is hozzáadunk itt
$form->onAnchor[] = fn() =>
$city->setItems($country->getValue()
? $this->world->getCities($country->getValue())
: []);
// $form->onSuccess[] = ...
return $form;
}
}
Az így létrehozott űrlap JavaScript nélkül is működni fog. Ez úgy történik, hogy a felhasználó először kiválaszt egy országot, elküldi az űrlapot, majd megjelenik a városok menüje, kiválaszt egyet közülük, és ismét elküldi az űrlapot.
Minket azonban az érdekel, hogy a városokat dinamikusan, JavaScript
segítségével töltsük be. Ezt a legtisztább módon a data-
attribútumok használatával közelíthetjük meg, amelyben információt
küldünk a HTML-nek (és így a JS-nek) arról, hogy mely kiválasztó dobozok
vannak összekapcsolva, és honnan kell adatokat lekérni.
Minden egyes gyermek selectboxhoz átadunk egy data-depends
attribútumot a szülőelem nevével, majd vagy egy data-url
attribútumot az URL-címmel, ahonnan AJAX segítségével lekérdezzük az
elemeket, vagy egy data-items
attribútumot, ahol közvetlenül
felsoroljuk az összes opciót.
Kezdjük az AJAX-változattal. Átadjuk a country
szülőelem
nevét és egy hivatkozást a Endpoint:cities
címre. A
#
karaktert helyőrzőként használjuk, és a JavaScript a
felhasználó által kiválasztott kulcsot helyezi el helyette.
$city = $form->addSelect('city', 'City:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-url', $this->link('Endpoint:cities', '#'));
És az AJAX nélküli változat? Előkészítünk egy tömböt az összes
országból és az összes városból, amelyet átadunk a
data-items
attribútumnak:
$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 kezelő
Az alábbi kód univerzális, nem kötődik a példában szereplő
country
és city
kiválasztó dobozokhoz, hanem az
oldal bármelyik kiválasztó dobozát összekapcsolja, csak állítsa be az
említett data-
attribútumokat.
A kód tiszta vanilla JS-ben íródott, tehát nem igényel jQuery-t vagy más könyvtárat.
// megtalálja az összes gyermek selectboxot az oldalon
document.querySelectorAll('select[data-depends]').forEach((childSelect) => {
let parentSelect = childSelect.form[childSelect.dataset.depends]; // szülő <select>
let url = childSelect.dataset.url; // attribútum data-url
let items = JSON.parse(childSelect.dataset.items || 'null'); // attribútum data-items
// amikor a felhasználó megváltoztatja a kiválasztott elemet a szülő kiválasztásban...
parentSelect.addEventListener('change', () => {
// ha a data-items attribútum létezik...
if (items) {
// új elemek betöltése közvetlenül a gyermek kiválasztó dobozba
updateSelectbox(childSelect, items[parentSelect.value]);
}
// ha a data-url attribútum létezik...
if (url) {
// AJAX-kérést küldünk a végpontra a kiválasztott elemmel a helyőrző helyett.
fetch(url.replace(encodeURIComponent('#'), encodeURIComponent(parentSelect.value)))
.then((response) => response.json())
// és új elemeket töltünk be a gyermek selectboxba
.then((data) => updateSelectbox(childSelect, data));
}
});
});
// helyettesíti a <options> címet a <select>
function updateSelectbox(select, items)
{
select.innerHTML = ''; // oldalon remove all
for (let id in items) { // új beillesztése
let el = document.createElement('option');
el.setAttribute('value', id);
el.innerText = items[id];
select.appendChild(el);
}
}
Több elem és újrafelhasználhatóság
A megoldás nem korlátozódik két kiválasztó mezőre, létrehozhat három vagy több függő elemből álló kaszkádot. Például hozzáadunk egy utca-kiválasztást, amely a kiválasztott várostól függ:
$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()) : []);
Emellett több kijelölőmező is függhet egyetlen közös mezőtől. Csak
állítsuk be a data-
attribútumokat analóg módon, és töltsük
fel az elemeket a setItems()
címmel.
Egyáltalán nincs szükség semmilyen módosításra a JavaScript-kódon, amely univerzálisan működik.
Biztonság
Még ezekben a példákban is megmarad a Nette űrlapok összes biztonsági mechanizmusa. Különösen minden egyes kiválasztási mező ellenőrzi, hogy a kiválasztott opció a felkínált lehetőségek egyike-e, és így egy támadó nem tud más értéket meghamisítani.
A megoldás a Nette 2.4 és újabb verziókban működik, a kódminták
PHP 8-ra íródtak. Ahhoz, hogy régebbi verziókban is működjenek,
helyettesítse a property
promotion és a fn()
szót a
function () use (...) { ... }
címmel.
A hozzászólás elküldéséhez kérjük, jelentkezzen be