Select boxes dépendants élégamment en Nette et JavaScript pur
Comment créer des select boxes liés, où après avoir choisi une valeur dans l'un, les options de l'autre se chargent dynamiquement ? En Nette et en JavaScript pur, c'est une tâche facile. Nous allons montrer une solution propre, réutilisable et sécurisée.

Modèle de données
Comme exemple, nous allons créer un formulaire contenant des select boxes pour choisir un pays et une ville.
Préparons d'abord le modèle de données qui retournera les éléments pour les deux select boxes. Il les obtiendra probablement d'une base de données. L'implémentation exacte n'est pas importante, nous allons donc juste esquisser à quoi ressemblera l'interface :
class World
{
public function getCountries(): array
{
return ...
}
public function getCities($country): array
{
return ...
}
}
Comme le nombre total de villes est vraiment grand, nous les obtiendrons via
AJAX. À cette fin, nous créerons un EndpointPresenter
,
c'est-à-dire une API qui nous retournera les villes de chaque pays au format
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);
}
}
S'il y avait peu de villes (peut-être sur une autre planète 😉), ou si le
modèle représentait des données qui ne sont tout simplement pas nombreuses,
nous pourrions les transmettre toutes directement sous forme de tableau à
JavaScript et économiser les requêtes AJAX. Dans ce cas,
EndpointPresenter
ne serait pas nécessaire.
Formulaire
Et passons au formulaire lui-même. Nous créerons deux select boxes et les
lierons, c'est-à-dire que nous définirons les éléments du subordonné
(city
) en fonction de la valeur choisie du supérieur
(country
). Il est important de le faire dans le gestionnaire
d'événements onAnchor,
c'est-à-dire au moment où le formulaire connaît déjà les valeurs envoyées
par l'utilisateur.
class DemoPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private World $world,
) {}
protected function createComponentForm(): Form
{
$form = new Form;
$country = $form->addSelect('country', 'Pays:', $this->world->getCountries())
->setPrompt('----');
$city = $form->addSelect('city', 'Ville:');
// <-- nous ajouterons quelque chose ici plus tard
$form->onAnchor[] = fn() =>
$city->setItems($country->getValue()
? $this->world->getCities($country->getValue())
: []);
// $form->onSuccess[] = ...
return $form;
}
}
Le formulaire ainsi créé fonctionnera même sans JavaScript. Et ce, de telle sorte que l'utilisateur choisit d'abord le pays, envoie le formulaire, puis l'offre de villes apparaît, il en choisit une et renvoie le formulaire.
Mais ce qui nous intéresse, c'est le chargement dynamique des villes à
l'aide de JavaScript. La manière la plus propre d'aborder cela est d'utiliser
les attributs data-
, dans lesquels nous envoyons à HTML (et donc
à JS) l'information sur les select boxes qui sont liés et d'où les données
doivent être extraites.
À chaque select box subordonné, nous transmettrons l'attribut
data-depends
avec le nom de l'élément parent et ensuite soit
data-url
avec l'URL d'où il doit obtenir les éléments via AJAX,
soit data-items
, où nous indiquerons directement toutes les
variantes.
Commençons par la variante AJAX. Nous transmettons le nom de l'élément
parent country
et le lien vers Endpoint:cities
. Nous
utilisons le caractère #
comme placeholder et JavaScript insérera
à sa place la clé choisie par l'utilisateur.
$city = $form->addSelect('city', 'Ville:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-url', $this->link('Endpoint:cities', '#'));
Et la variante sans AJAX ? Nous préparons un tableau de tous les pays et de
toutes leurs villes, que nous transmettons à l'attribut
data-items
:
$items = [];
foreach ($this->world->getCountries() as $id => $name) {
$items[$id] = $this->world->getCities($id);
}
$city = $form->addSelect('city', 'Ville:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-items', $items);
Et il ne reste plus qu'à écrire le gestionnaire JavaScript.
Gestionnaire JavaScript
Le code suivant est universel, il n'est pas lié aux select boxes
spécifiques country
et city
de l'exemple, mais liera
n'importe quels select boxes sur la page, il suffit de leur définir les
attributs data-
mentionnés.
Le code est écrit en pur vanilla JS, il ne nécessite donc pas jQuery ou une autre bibliothèque.
// trouve tous les select boxes enfants sur la page
document.querySelectorAll('select[data-depends]').forEach((childSelect) => {
let parentSelect = childSelect.form[childSelect.dataset.depends]; // le <select> parent
let url = childSelect.dataset.url; // l'attribut data-url
let items = JSON.parse(childSelect.dataset.items || 'null'); // l'attribut data-items
// lorsque l'utilisateur change l'élément sélectionné dans le select parent...
parentSelect.addEventListener('change', () => {
// s'il existe un attribut data-items...
if (items) {
// nous chargeons directement les nouveaux éléments dans le select box enfant
updateSelectbox(childSelect, items[parentSelect.value]);
}
// s'il existe un attribut data-url...
if (url) {
// nous faisons une requête AJAX vers l'endpoint avec l'élément sélectionné à la place du placeholder
fetch(url.replace(encodeURIComponent('#'), encodeURIComponent(parentSelect.value)))
.then((response) => response.json())
// et nous chargeons les nouveaux éléments dans le select box enfant
.then((data) => updateSelectbox(childSelect, data));
}
});
});
// réécrit les <options> dans le <select>
function updateSelectbox(select, items)
{
select.innerHTML = ''; // nous supprimons tout
for (let id in items) { // nous insérons les nouveaux
let el = document.createElement('option');
el.setAttribute('value', id);
el.innerText = items[id];
select.appendChild(el);
}
}
Plusieurs éléments et réutilisabilité
La solution n'est pas limitée à deux select boxes, il est possible de créer une cascade de trois éléments ou plus dépendants les uns des autres. Par exemple, nous ajoutons le choix de la rue, qui dépendra de la ville choisie :
$street = $form->addSelect('street', 'Rue:')
->setHtmlAttribute('data-depends', $city->getHtmlName())
->setHtmlAttribute('data-url', $this->link('Endpoint:streets', '#'));
$form->onAnchor[] = fn() =>
$street->setItems($city->getValue() ? $this->world->getStreets($city->getValue()) : []);
Plusieurs select boxes peuvent également dépendre d'un seul élément
commun. Il suffit de définir de manière analogue les attributs
data-
et de remplir les éléments à l'aide de
setItems()
.
Il n'est pas nécessaire d'apporter de modification au code JavaScript, qui fonctionne de manière universelle.
Sécurité
Même dans ces exemples, tous les mécanismes de sécurité dont disposent les formulaires Nette sont toujours préservés. Notamment, chaque select box vérifie que la variante sélectionnée est l'une des options proposées et donc qu'un attaquant ne peut pas soumettre une autre valeur.
La solution fonctionne avec Nette 2.4 et plus, les exemples de code sont
écrits pour PHP 8. Pour les faire fonctionner dans des versions plus
anciennes, remplacez property
promotion et fn()
par
function () use (...) { ... }
.
Pour soumettre un commentaire, veuillez vous connecter