Caselle di selezione dipendenti in modo elegante in Nette e puro JS
Come creare selectbox concatenate, in cui dopo aver selezionato un valore in una, le opzioni vengono aggiornate dinamicamente nell'altra? Questo è un compito facile in Nette e puro JavaScript. Mostreremo una soluzione pulita, riutilizzabile e sicura.
Modello di dati
A titolo di esempio, creiamo un modulo contenente caselle di selezione per la scelta del Paese e della città.
Per prima cosa, prepareremo un modello di dati che restituirà le voci per entrambe le caselle di selezione. Probabilmente li recupererà dal database. L'implementazione esatta non è essenziale, quindi accenniamo solo all'aspetto dell'interfaccia:
class World
{
public function getCountries(): array
{
return ...
}
public function getCities($country): array
{
return ...
}
}
Poiché il numero totale di città è molto grande, le recupereremo
utilizzando AJAX. A tale scopo, creeremo una EndpointPresenter
,
un'API che restituirà le città di ogni Paese in formato 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);
}
}
Se le città sono poche (ad esempio su un altro pianeta 😉 ), o se il
modello rappresenta dati che semplicemente non sono molti, potremmo passarli
tutti come array a JavaScript e risparmiare richieste AJAX. In questo caso, non
ci sarebbe bisogno di EndpointPresenter
.
Modulo
Passiamo al modulo stesso. Creeremo due caselle di selezione e le
collegheremo, cioè imposteremo gli elementi figli (city
) a seconda
del valore selezionato del genitore (country
). L'importante è
farlo nel gestore dell'evento onAnchor,
cioè nel momento in cui il modulo conosce già i valori inviati
dall'utente.
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:');
// <-- aggiungeremo qualcos'altro qui
$form->onAnchor[] = fn() =>
$city->setItems($country->getValue()
? $this->world->getCities($country->getValue())
: []);
// $form->onSuccess[] = ...
return $form;
}
}
Il modulo così creato funzionerà senza JavaScript. Per fare ciò, l'utente deve prima selezionare un paese, inviare il modulo, poi apparirà un menu di città, selezionarne una e inviare nuovamente il modulo.
Tuttavia, a noi interessa caricare dinamicamente le città utilizzando
JavaScript. Il modo più pulito per farlo è usare gli attributi
data-
, con i quali si inviano informazioni all'HTML (e quindi a
JS) su quali caselle di selezione sono collegate e da dove recuperare
i dati.
Per ogni casella di selezione figlia, passiamo un attributo
data-depends
con il nome dell'elemento padre e poi un attributo
data-url
con l'URL da cui recuperare gli elementi tramite AJAX,
oppure un attributo data-items
in cui elenchiamo direttamente tutte
le opzioni.
Cominciamo con la variante AJAX. Passiamo il nome dell'elemento padre
country
e un riferimento a Endpoint:cities
. Usiamo il
carattere #
come segnaposto e JavaScript inserirà invece la chiave
selezionata dall'utente.
$city = $form->addSelect('city', 'City:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-url', $this->link('Endpoint:cities', '#'));
E la variante senza AJAX? Prepariamo un array di tutti i paesi e di tutte le
loro città, che passiamo all'attributo 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);
Gestore JavaScript
Il codice seguente è universale, non è legato alle caselle di selezione
specifiche country
e city
dell'esempio, ma collega
qualsiasi casella di selezione della pagina, basta impostare gli attributi
data-
citati.
Il codice è scritto in puro JS vanilla, quindi non richiede jQuery o altre librerie.
// trova tutte le selectbox figlio della pagina
document.querySelectorAll('select[data-depends]').forEach((childSelect) => {
let parentSelect = childSelect.form[childSelect.dataset.depends]; // genitore <select>
let url = childSelect.dataset.url; // attributo data-url
let items = JSON.parse(childSelect.dataset.items || 'null'); // attributo data-items
// quando l'utente cambia l'elemento selezionato nella selezione padre...
parentSelect.addEventListener('change', () => {
// se l'attributo data-items esiste...
if (items) {
// carica i nuovi elementi direttamente nella casella di selezione figlio
updateSelectbox(childSelect, items[parentSelect.value]);
}
// se l'attributo data-url esiste...
if (url) {
// facciamo una richiesta AJAX all'endpoint con l'elemento selezionato invece del segnaposto
fetch(url.replace(encodeURIComponent('#'), encodeURIComponent(parentSelect.value)))
.then((response) => response.json())
// e carichiamo i nuovi elementi nel selectbox figlio
.then((data) => updateSelectbox(childSelect, data));
}
});
});
// sostituisce <options> in <select>
function updateSelectbox(select, items)
{
select.innerHTML = ''; // rimuove tutto
for (let id in items) { // inserire nuovi elementi
let el = document.createElement('option');
el.setAttribute('value', id);
el.innerText = items[id];
select.appendChild(el);
}
}
Più elementi e riutilizzabilità
La soluzione non si limita a due caselle di selezione, ma è possibile creare una cascata di tre o più elementi dipendenti. Ad esempio, aggiungiamo una selezione di strade che dipende dalla città selezionata:
$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()) : []);
Inoltre, più caselle di selezione possono dipendere da una singola casella
comune. Basta impostare gli attributi data-
per analogia e popolare
gli elementi con setItems()
.
Non è assolutamente necessario modificare il codice JavaScript, che funziona universalmente.
Sicurezza
Anche in questi esempi, tutti i meccanismi di sicurezza dei moduli Nette sono stati mantenuti. In particolare, ogni casella di selezione controlla che l'opzione selezionata sia una di quelle proposte e quindi un aggressore non può falsificare un valore diverso.
La soluzione funziona con Nette 2.4 e successive, mentre gli esempi di
codice sono stati scritti per PHP 8. Per farli funzionare nelle versioni
precedenti, è necessario che il codice sia stato scritto per PHP 8. Per farli
funzionare nelle versioni precedenti, sostituire property
promotion e fn()
con
function () use (...) { ... }
.
Per inviare un commento, effettuare il login