Selectbox dipendenti elegantemente in Nette e JavaScript puro
Come creare selectbox collegati, in cui dopo aver scelto un valore in uno, le opzioni vengono caricate dinamicamente nel secondo? In Nette e JavaScript puro è un compito facile. Mostreremo una soluzione pulita, riutilizzabile e sicura.

Modello di dati
Come esempio, creeremo un form contenente selectbox per la scelta dello stato e della città.
Innanzitutto, prepareremo un modello di dati che restituirà gli elementi per entrambi i selectbox. Probabilmente li otterrà da un database. L'implementazione esatta non è importante, quindi indicheremo solo come apparirà l'interfaccia:
class World
{
public function getCountries(): array
{
return ...
}
public function getCities($country): array
{
return ...
}
}
Poiché il numero totale di città è davvero grande, le otterremo tramite
AJAX. A tale scopo, creeremo un EndpointPresenter
, ovvero un'API
che ci restituirà le città nei singoli stati come 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à fossero poche (magari su un altro pianeta 😉), o se il
modello rappresentasse dati che semplicemente non sono molti, potremmo passarli
tutti direttamente come array a JavaScript e risparmiare richieste AJAX. In tal
caso, EndpointPresenter
non sarebbe necessario.
Form
E passiamo al form stesso. Creeremo due selectbox e li collegheremo, cioè
imposteremo gli elementi del subordinato (city
) in base al valore
selezionato del superiore (country
). È importante farlo nel
gestore dell'evento onAnchor,
cioè nel momento in cui il form 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', 'Stato:', $this->world->getCountries())
->setPrompt('----');
$city = $form->addSelect('city', 'Città:');
// <-- qui aggiungeremo qualcos'altro
$form->onAnchor[] = fn() =>
$city->setItems($country->getValue()
? $this->world->getCities($country->getValue())
// Se nessun paese è selezionato, restituisce un array vuoto
: []);
// $form->onSuccess[] = ...
return $form;
}
}
Un form creato in questo modo funzionerà anche senza JavaScript. E cioè, l'utente seleziona prima lo stato, invia il form, quindi appare l'offerta di città, ne seleziona una e invia nuovamente il form.
Ma a noi interessa il caricamento dinamico delle città tramite JavaScript.
Il modo più pulito per farlo è utilizzare attributi data-
, nei
quali invieremo a HTML (e quindi a JS) informazioni su quali selectbox sono
collegati e da dove devono essere recuperati i dati.
A ogni selectbox subordinato passeremo l'attributo data-depends
con il nome dell'elemento superiore e poi o data-url
con l'URL da
cui recuperare gli elementi tramite AJAX, oppure data-items
, dove
elencheremo direttamente tutte le varianti.
Iniziamo con la variante AJAX. Passiamo il nome dell'elemento superiore
country
e il link a Endpoint:cities
. Usiamo il
carattere #
come placeholder e JavaScript inserirà al suo posto la
chiave selezionata dall'utente.
$city = $form->addSelect('city', 'Città:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-url', $this->link('Endpoint:cities', '#'));
E la variante senza AJAX? Prepariamo un array di tutti gli stati e di tutte
le loro città, che passeremo all'attributo data-items
:
$items = [];
foreach ($this->world->getCountries() as $id => $name) {
$items[$id] = $this->world->getCities($id);
}
$city = $form->addSelect('city', 'Città:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-items', json_encode($items)); // Codifica in JSON
E resta da scrivere il gestore JavaScript.
Gestore JavaScript
Il codice seguente è universale, non è legato ai selectbox specifici
country
e city
dell'esempio, ma collegherà qualsiasi
selectbox sulla pagina, basta impostare loro i suddetti attributi
data-
.
Il codice è scritto in puro vanilla JS, quindi non richiede jQuery o altre librerie.
// troviamo sulla pagina tutti i selectbox subordinati
document.querySelectorAll('select[data-depends]').forEach((childSelect) => {
let parentSelect = childSelect.form[childSelect.dataset.depends]; // il <select> superiore
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 nel select superiore...
parentSelect.addEventListener('change', () => {
// se esiste l'attributo data-items...
if (items) {
// carichiamo direttamente nel selectbox subordinato i nuovi elementi
updateSelectbox(childSelect, items[parentSelect.value] || {}); // Usa un oggetto vuoto se la chiave non esiste
}
// se esiste l'attributo data-url...
if (url) {
// facciamo una richiesta AJAX all'endpoint con l'elemento selezionato al posto del placeholder
fetch(url.replace(encodeURIComponent('#'), encodeURIComponent(parentSelect.value)))
.then((response) => response.json())
// e carichiamo nel selectbox subordinato i nuovi elementi
.then((data) => updateSelectbox(childSelect, data));
}
});
// Attiva l'evento change all'inizio per caricare i dati iniziali se un valore è già selezionato
parentSelect.dispatchEvent(new Event('change'));
});
// sovrascrive le <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 è limitata a due selectbox, è possibile creare tranquillamente una cascata di tre o più elementi dipendenti l'uno dall'altro. Ad esempio, aggiungiamo la scelta della via, che dipenderà dalla città selezionata:
$street = $form->addSelect('street', 'Via:')
->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ù selectbox possono dipendere da uno comune. Basta impostare
analogamente gli attributi data-
e popolare gli elementi tramite
setItems()
.
Senza dover apportare alcuna modifica al codice JavaScript, che funziona universalmente.
Sicurezza
Anche in questi esempi vengono mantenuti tutti i meccanismi di sicurezza di cui dispongono i form in Nette. In particolare, ogni selectbox controlla che la variante selezionata sia una di quelle offerte e quindi un attaccante non può sostituire un valore diverso.
La soluzione funziona in Nette 2.4 e versioni successive, gli esempi di
codice sono scritti per Nette per PHP 8. Per farli funzionare nelle versioni
precedenti, sostituisci la property
promotion e fn()
con
function () use (...) { ... }
.
Per inviare un commento, effettuare il login