Casete de selectare dependente în mod elegant în Nette și JS pur
Cum să creați casete de selectare înlănțuite, unde după selectarea unei valori în una dintre ele, opțiunile sunt actualizate dinamic în cealaltă? Aceasta este o sarcină ușoară în Nette și JavaScript pur. Vom arăta o soluție care este curată, reutilizabilă și sigură.
Model de date
Ca exemplu, să creăm un formular care conține căsuțe de selectare pentru selectarea țării și a orașului.
În primul rând, vom pregăti un model de date care va returna intrări pentru ambele casete de selectare. Acesta le va prelua probabil din baza de date. Implementarea exactă nu este esențială, așa că vom face doar o aluzie la modul în care 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 prelua folosind
AJAX. În acest scop, vom crea un EndpointPresenter
, un API care va
returna orașele din fiecare țară sub formă de 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ă există puține orașe (de exemplu, pe o altă planetă 😉 ) sau
dacă modelul reprezintă date care pur și simplu nu sunt multe, am putea să
le trecem pe toate ca matrice în JavaScript și să economisim cererile AJAX.
În acest caz, nu ar mai fi nevoie de EndpointPresenter
.
Formular
Și să trecem la formularul propriu-zis. Vom crea două casete de selectare
și le vom lega, adică vom seta elementele copilului (city
) în
funcție de valoarea selectată a părintelui (country
). Important
este că vom face acest lucru în gestionarul de evenimente 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', 'Country:', $this->world->getCountries())
->setPrompt('----');
$city = $form->addSelect('city', 'City:');
// <-- vom mai adăuga ceva aici
$form->onAnchor[] = fn() =>
$city->setItems($country->getValue()
? $this->world->getCities($country->getValue())
: []);
// $form->onSucces[] = ...
return $form;
}
}
Formularul creat în acest mod va funcționa fără JavaScript. Acest lucru se realizează prin faptul că utilizatorul va selecta mai întâi o țară, va trimite formularul, apoi va apărea un meniu de orașe, va selecta unul dintre ele și va trimite din nou formularul.
Cu toate acestea, suntem interesați de încărcarea dinamică a orașelor
folosind JavaScript. Cea mai curată modalitate de abordare este utilizarea
atributelor data-
, prin care trimitem informații către HTML (și,
prin urmare, către JS) despre ce casete de selectare sunt legate și de unde
să recuperăm datele.
Pentru fiecare selectbox copil, transmitem un atribut
data-depends
cu numele elementului părinte și apoi fie un atribut
data-url
cu URL-ul de la care să recuperăm elementele folosind
AJAX, fie un atribut data-items
în care enumerăm direct toate
opțiunile.
Să începem cu varianta AJAX. Transmitem numele elementului părinte
country
și o referință la Endpoint:cities
. Folosim
caracterul #
ca un spațiu rezervat, iar JavaScript va pune în loc
cheia selectată de utilizator.
$city = $form->addSelect('city', 'City:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-url', $this->link('Endpoint:cities', '#'));
Și varianta fără AJAX? Pregătim o matrice cu toate țările și toate
orașele sale, pe care o trecem la atributul 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);
Manipulator JavaScript
Următorul cod este universal, nu este legat de căsuțele de selectare
specifice country
și city
din exemplu, dar va lega
orice căsuță de selectare de pe pagină, doar dacă setați atributele
data-
menționate.
Codul este scris în JS pur vanilla, deci nu necesită jQuery sau altă bibliotecă.
// găsește toate căsuțele de selectare copil de pe pagină
document.querySelectorAll('select[data-depends]').forEach((childSelect) => {
let parentSelect = childSelect.form[childSelect.dataset.depends]; // părinte <select>
let url = childSelect.dataset.url; // atributul data-url
let items = JSON.parse(childSelect.dataset.items || 'null'); // attribute data-items
// atunci când utilizatorul modifică elementul selectat în selecția părinte...
parentSelect.addEventListener('change', () => {
// dacă atributul data-items există...
if (items) {
// încarcă noile elemente direct în caseta de selectare copil
updateSelectbox(childSelect, items[parentSelect.value]);
}
// dacă există atributul data-url...
if (url) {
// se face o cerere AJAX către punctul final cu elementul selectat în loc de placeholder.
fetch(url.replace(encodeURIComponent('#'), encodeURIComponent(parentSelect.value)))
.then((response) => response.json())
// și încărcăm noi elemente în caseta de selectare copil
.then((data) => updateSelectbox(childSelect, data));
}
});
});
// înlocuiește <options> în <select>
function updateSelectbox(select, items)
{
select.innerHTML = ''; // remove all
for (let id in items) { // introduceți un nou element
let el = document.createElement('option');
el.setAttribute('value', id);
el.innerText = items[id];
select.appendChild(el);
}
}
Mai multe elemente și posibilitatea de reutilizare
Soluția nu se limitează la două casete de selectare, ci puteți crea o cascadă de trei sau mai multe elemente dependente. De exemplu, adăugăm o selecție de străzi care depinde de orașul selectat:
$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()) : []);
De asemenea, mai multe casete de selectare pot depinde de una singură
comună. Trebuie doar să setați atributele data-
prin analogie
și să populați elementele cu setItems()
.
Nu este absolut deloc necesar să faceți nicio modificare a codului JavaScript, care funcționează universal.
Securitate
Chiar și în aceste exemple, toate mecanismele de securitate pe care le au formularele Nette sunt păstrate. În special, fiecare căsuță de selectare verifică dacă opțiunea selectată este una dintre cele oferite și, astfel, un atacator nu poate falsifica o valoare diferită.
Soluția funcționează în Nette 2.4 și ulterior, exemplele de cod
sunt scrise pentru PHP 8. Pentru a le face să funcționeze î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