Selects dependientes de forma elegante en Nette y JavaScript puro
¿Cómo crear selects dependientes, donde al elegir un valor en uno, las opciones del otro se cargan dinámicamente? En Nette y JavaScript puro es una tarea fácil. Mostraremos una solución limpia, reutilizable y segura.

Modelo de datos
Como ejemplo, crearemos un formulario que contenga selects para elegir país y ciudad.
Primero, prepararemos un modelo de datos que devolverá los elementos para ambos selects. Probablemente los obtendrá de una base de datos. La implementación exacta no es importante, por lo que solo indicaremos cómo será la interfaz:
class World
{
public function getCountries(): array
{
return ...
}
public function getCities($country): array
{
return ...
}
}
Dado que el número total de ciudades es realmente grande, las obtendremos
mediante AJAX. Para este propósito, crearemos un
EndpointPresenter
, es decir, una API que nos devolverá las
ciudades de cada país como 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);
}
}
Si hubiera pocas ciudades (quizás en otro planeta 😉), o si el modelo
representara datos que simplemente no son muchos, podríamos pasarlos todos
directamente como un array a JavaScript y ahorrar peticiones AJAX. En tal caso,
no sería necesario el EndpointPresenter
.
Formulario
Y pasemos al formulario propiamente dicho. Crearemos dos cajas de selección
y las enlazaremos, es decir, estableceremos los elementos hijos
(city
) en función del valor seleccionado del padre
(country
). Lo importante es que lo hagamos en el manejador del
evento onAnchor,
es decir, en el momento en que el formulario ya conoce los valores enviados por
el usuario.
class DemoPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private World $world,
) {}
protected function createComponentForm(): Form
{
$form = new Form;
$country = $form->addSelect('country', 'País:', $this->world->getCountries())
->setPrompt('----');
$city = $form->addSelect('city', 'Ciudad:');
// <-- aquí añadiremos algo más tarde
$form->onAnchor[] = fn() =>
$city->setItems($country->getValue()
? $this->world->getCities($country->getValue())
: []);
// $form->onSuccess[] = ...
return $form;
}
}
Un formulario creado de esta manera funcionará incluso sin JavaScript. Y lo hará de forma que el usuario primero seleccione un país, envíe el formulario, luego aparecerá la oferta de ciudades, seleccionará una de ellas y enviará el formulario de nuevo.
Pero a nosotros nos interesa la carga dinámica de ciudades mediante
JavaScript. La forma más limpia de abordar esto es utilizar atributos
data-
, en los que enviaremos a HTML (y por extensión a JS)
información sobre qué selects están vinculados y de dónde deben obtener
los datos.
A cada select secundario le pasaremos el atributo data-depends
con el nombre del elemento principal y luego, o bien data-url
con
la URL desde donde debe obtener los elementos mediante AJAX, o
data-items
, donde indicaremos directamente todas las variantes.
Comencemos con la variante AJAX. Pasaremos el nombre del elemento principal
country
y el enlace a Endpoint:cities
. Usamos el
carácter #
como placeholder y JavaScript insertará en su lugar la
clave elegida por el usuario.
$city = $form->addSelect('city', 'Ciudad:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-url', $this->link('Endpoint:cities', '#'));
¿Y la variante sin AJAX? Prepararemos un array de todos los países y todas
sus ciudades, que pasaremos al atributo data-items
:
$items = [];
foreach ($this->world->getCountries() as $id => $name) {
$items[$id] = $this->world->getCities($id);
}
$city = $form->addSelect('city', 'Ciudad:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-items', $items);
Y solo queda escribir el manejador de JavaScript.
Manejador de JavaScript
El siguiente código es universal, no está vinculado a los selects
específicos country
y city
del ejemplo, sino que
vinculará cualquier select en la página, solo es necesario establecerles los
atributos data-
mencionados.
El código está escrito en vanilla JS puro, por lo que no requiere jQuery ni ninguna otra librería.
// encontramos todos los selects secundarios en la página
document.querySelectorAll('select[data-depends]').forEach((childSelect) => {
let parentSelect = childSelect.form[childSelect.dataset.depends]; // el <select> padre
let url = childSelect.dataset.url; // atributo data-url
let items = JSON.parse(childSelect.dataset.items || 'null'); // atributo data-items
// cuando el usuario cambia la opción seleccionada en el select padre...
parentSelect.addEventListener('change', () => {
// si existe el atributo data-items...
if (items) {
// cargamos directamente nuevas opciones en el select secundario
updateSelectbox(childSelect, items[parentSelect.value]);
}
// si existe el atributo data-url...
if (url) {
// hacemos una petición AJAX al endpoint con la opción seleccionada en lugar del placeholder
fetch(url.replace(encodeURIComponent('#'), encodeURIComponent(parentSelect.value)))
.then((response) => response.json())
// y cargamos nuevas opciones en el select secundario
.then((data) => updateSelectbox(childSelect, data));
}
});
});
// reescribe los <options> en el <select>
function updateSelectbox(select, items)
{
select.innerHTML = ''; // eliminamos todo
for (let id in items) { // insertamos los nuevos
let el = document.createElement('option');
el.setAttribute('value', id);
el.innerText = items[id];
select.appendChild(el);
}
}
Múltiples elementos y reutilización
La solución no está limitada a dos selects, se puede crear tranquilamente una cascada de tres o más elementos dependientes entre sí. Por ejemplo, añadimos la elección de la calle, que dependerá de la ciudad elegida:
$street = $form->addSelect('street', 'Calle:')
->setHtmlAttribute('data-depends', $city->getHtmlName())
->setHtmlAttribute('data-url', $this->link('Endpoint:streets', '#'));
$form->onAnchor[] = fn() =>
$street->setItems($city->getValue() ? $this->world->getStreets($city->getValue()) : []);
También pueden depender varios selects de uno común. Basta con establecer
análogamente los atributos data-
y rellenar las opciones mediante
setItems()
.
Sin necesidad de hacer ninguna intervención en el código JavaScript, que funciona universalmente.
Seguridad
Incluso en estos ejemplos, se mantienen todos los mecanismos de seguridad de los que disponen los formularios en Nette. En particular, que cada select controla que la variante seleccionada sea una de las ofrecidas y, por lo tanto, un atacante no puede colar otro valor.
La solución funciona en Nette 2.4 y posteriores, los ejemplos de
código están escritos para PHP 8. Para que funcionen en versiones anteriores,
sustituya property
promotion y fn()
por
function () use (...) { ... }
.
Para enviar un comentario, inicie sesión