Selectboxes dependentes elegantemente em Nette e JavaScript puro
Como criar selectboxes interligados, onde após escolher um valor em um, as opções do outro são carregadas dinamicamente? Em Nette e JavaScript puro, esta é uma tarefa fácil. Mostraremos uma solução limpa, reutilizável e segura.

Modelo de dados
Como exemplo, criaremos um formulário contendo selectboxes para escolher país e cidade.
Primeiro, prepararemos o modelo de dados que retornará os itens para ambos os selectboxes. Provavelmente, ele os obterá de um banco de dados. A implementação exata não é importante, por isso apenas indicaremos como será a interface:
class World
{
public function getCountries(): array
{
return ...
}
public function getCities($country): array
{
return ...
}
}
Como o número total de cidades é realmente grande, vamos obtê-las usando
AJAX. Para este fim, criaremos um EndpointPresenter
, ou seja, uma
API que nos retornará as cidades 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);
}
}
Se houvesse poucas cidades (talvez em outro planeta 😉), ou se o modelo
representasse dados que simplesmente não são muitos, poderíamos passá-los
todos de uma vez como um array para o JavaScript e economizar requisições
AJAX. Nesse caso, o EndpointPresenter
não seria necessário.
Formulário
E passemos à forma propriamente dita. Vamos criar duas caixas de seleção e
ligá-las, ou seja, vamos definir os itens da criança (city
)
dependendo do valor selecionado dos pais (country
). O importante
é que façamos isso no manipulador de eventos onAnchor,
ou seja, no momento em que o formulário já conhece os valores enviados pelo
usuário.
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', 'Cidade:');
// <-- adicionaremos algo aqui mais tarde
$form->onAnchor[] = fn() =>
$city->setItems($country->getValue()
? $this->world->getCities($country->getValue())
: []);
// $form->onSuccess[] = ...
return $form;
}
}
Um formulário criado desta forma funcionará mesmo sem JavaScript. E funciona assim: o usuário primeiro seleciona o país, envia o formulário, depois aparece a oferta de cidades, ele seleciona uma delas e envia o formulário novamente.
Mas estamos interessados no carregamento dinâmico de cidades usando
JavaScript. A maneira mais limpa de abordar isso é usar atributos
data-
, nos quais enviamos informações para o HTML (e, por
extensão, JS) sobre quais selectboxes estão interligados e de onde os dados
devem ser obtidos.
Para cada selectbox dependente, passaremos o atributo
data-depends
com o nome do elemento pai e, em seguida,
data-url
com a URL de onde obter os itens via AJAX, ou
data-items
, onde listamos todas as variantes diretamente.
Comecemos com a variante AJAX. Passamos o nome do elemento pai
country
e o link para Endpoint:cities
. Usamos
o caractere #
como placeholder e o JavaScript inserirá a chave
selecionada pelo usuário em seu lugar.
$city = $form->addSelect('city', 'Cidade:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-url', $this->link('Endpoint:cities', '#'));
E a variante sem AJAX? Prepararemos um array de todos os países e todas as
suas cidades, que passaremos para o atributo data-items
:
$items = [];
foreach ($this->world->getCountries() as $id => $name) {
$items[$id] = $this->world->getCities($id);
}
$city = $form->addSelect('city', 'Cidade:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-items', $items);
E resta escrever o manipulador JavaScript.
Manipulador JavaScript
O código a seguir é universal, não está vinculado aos selectboxes
específicos country
e city
do exemplo, mas
interligará quaisquer selectboxes na página, basta definir os atributos
data-
mencionados.
O código está escrito em vanilla JS puro, portanto não requer jQuery ou outra biblioteca.
// encontramos todos os selectboxes filhos na página
document.querySelectorAll('select[data-depends]').forEach((childSelect) => {
let parentSelect = childSelect.form[childSelect.dataset.depends]; // <select> pai
let url = childSelect.dataset.url; // atributo data-url
let items = JSON.parse(childSelect.dataset.items || 'null'); // atributo data-items
// quando o usuário altera o item selecionado no select pai...
parentSelect.addEventListener('change', () => {
// se o atributo data-items existir...
if (items) {
// carregamos diretamente novos itens no selectbox filho
updateSelectbox(childSelect, items[parentSelect.value]);
}
// se o atributo data-url existir...
if (url) {
// fazemos uma requisição AJAX para o endpoint com o item selecionado em vez do placeholder
fetch(url.replace(encodeURIComponent('#'), encodeURIComponent(parentSelect.value)))
.then((response) => response.json())
// e carregamos novos itens no selectbox filho
.then((data) => updateSelectbox(childSelect, data));
}
});
});
// sobrescreve <options> em <select>
function updateSelectbox(select, items)
{
select.innerHTML = ''; // removemos tudo
for (let id in items) { // inserimos novos
let el = document.createElement('option');
el.setAttribute('value', id);
el.innerText = items[id];
select.appendChild(el);
}
}
Mais elementos e reutilização
A solução não está limitada a dois selectboxes, é possível criar uma cascata de três ou mais elementos dependentes entre si. Por exemplo, adicionamos a escolha da rua, que dependerá da cidade selecionada:
$street = $form->addSelect('street', 'Rua:')
->setHtmlAttribute('data-depends', $city->getHtmlName())
->setHtmlAttribute('data-url', $this->link('Endpoint:streets', '#'));
$form->onAnchor[] = fn() =>
$street->setItems($city->getValue() ? $this->world->getStreets($city->getValue()) : []);
Também podem existir vários selectboxes dependendo de um comum. Basta
definir analogamente os atributos data-
e preencher os itens usando
setItems()
.
Não é necessário fazer nenhuma alteração no código JavaScript, que funciona universalmente.
Segurança
Mesmo nestes exemplos, todos os mecanismos de segurança que os formulários Nette possuem são mantidos. Principalmente, que cada selectbox verifica se a variante selecionada é uma das oferecidas e, portanto, um atacante não pode submeter um valor diferente.
A solução funciona em Nette 2.4 e posteriormente, as amostras de
código são escritas para o PHP 8. Para fazê-los funcionar em versões
antigas, substitua promoção
de propriedade e fn()
por
function () use (...) { ... }
.
Faça o login para enviar um comentário