Přejít k navigační liště

Zdroják » JavaScript » Závislé selectboxy elegantně v Nette a čistém JavaScriptu

Závislé selectboxy elegantně v Nette a čistém JavaScriptu

Články JavaScript, PHP

Jak vytvořit provázané selectboxy, kdy po volbě hodnoty v jednom se dynamicky načtou volby do druhého? V Nette a čistém JavaScriptu jde o snadnou úlohu. Ukážeme si řešení, které je čisté, znovupoužitelné a bezpečné.

Nálepky:

Text vyšel původně na webu autora.

Datový model

Jako příklad si vytvoříme formulář obsahující selectboxy pro volbu státu a města.

Nejprve si připravíme datový model, který bude vracet položky pro oba selectboxy. Pravděpodobně je bude získávat z databáze. Přesná implementace není podstatná, proto jen naznačíme, jak bude vypadat rozhraní:

class World
{
	public function getCountries(): array
	{
		return ...
	}

	public function getCities($country): array
	{
		return ...
	}
}

Protože je celkový počet měst opravdu velký, budeme je získávat pomocí AJAXu. Pro tento účel si vytvoříme EndpointPresenter, tedy API, které nám bude vracet města v jednotlivých státech jako 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);
	}
}

Pokud by měst bylo málo (třeba na jiné planetě 😉), nebo by model reprezentoval data, kterých prostě není mnoho, mohli bychom je předat rovnou všechna jako pole do JavaScriptu a ušetřit AJAXové požadavky. V takém případě by nebyl EndpointPresenter potřeba.

Formulář

A pojďme na samotný formulář. Vytvoříme dva selectboxy a ty provážeme, tj. podřízenému (city) nastavíme položky v závislosti na zvolené hodnotě nadřízeného (country). Důležité je, že tak činíme v obsluze události onAnchor, tedy ve chvíli, kdy formulář už zná hodnoty odeslané uživatelem.

class DemoPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private World $world,
	) {}

	protected function createComponentForm(): Form
	{
		$form = new Form;
		$country = $form->addSelect('country', 'Stát:', $this->world->getCountries())
			->setPrompt('----');

		$city = $form->addSelect('city', 'Město:');
		// <-- sem pak ještě něco doplníme

		$form->onAnchor[] = fn() =>
			$city->setItems($country->getValue()
				? $this->world->getCities($country->getValue())
				: []);

		// $form->onSuccess[] = ...
		return $form;
	}
}

Takto vytvořený formulář bude fungovat i bez JavaScriptu. A to tak, že uživatel nejprve vybere stát, odešle formulář, poté se objeví nabídka měst, jedno z nich vybere a formulář odešle znovu.

Nás ale zajímá dynamické načítání měst pomocí JavaScriptu. Nejčistějším způsobem, jak k tomu přistoupit, je využít data- atributy, ve kterých si pošleme do HTML (a potažmo JS) informaci o tom, které selectboxy jsou provázané a odkud se mají čerpat data.

Každému podřízenému selectboxu předáme atribut data-depends s názvem nadřízeného prvku a dále buď data-url s URL, odkud má získávat položky pomocí AJAXu, nebo data-items, kde všechny varianty rovnou uvedeme.

Začněme s AJAXovou variantou. Předáme jméno nadřazeného prvku country a odkaz na Endpoint:cities. Znak # používáme jako placeholder a JavaScript bude místo něj vkládat uživatelem zvolený klíč.

$city = $form->addSelect('city', 'Město:')
	->setHtmlAttribute('data-depends', $country->getHtmlName())
	->setHtmlAttribute('data-url', $this->link('Endpoint:cities', '#'));

A varianta bez AJAXu? Připravíme si pole všech států a všech jejich měst, které předáme do atributu data-items:

$items = [];
foreach ($this->world->getCountries() as $id => $name) {
	$items[$id] = $this->world->getCities($id);
}

$city = $form->addSelect('city', 'Město:')
	->setHtmlAttribute('data-depends', $country->getHtmlName())
	->setHtmlAttribute('data-items', $items);

A zbývá napsat obslužný JavaScript.

JavaScriptová obsluha

Následující kód je univerzální, není vázaný na konkrétní selectboxy country a city z příkladu, ale prováže jakékoliv selectboxy na stránce, stačí jim jen nastavit zmíněné data- atributy.

Kód je napsaný v čistém vanilla JS, nevyžaduje tedy jQuery nebo jinou knihovnu.

// najdeme na stránce všechny podřízené selectboxy
document.querySelectorAll('select[data-depends]').forEach((childSelect) => {
	let parentSelect = childSelect.form[childSelect.dataset.depends]; // nadřízený <select>
	let url = childSelect.dataset.url; // atribut data-url
	let items = JSON.parse(childSelect.dataset.items || 'null'); // atribut data-items

	// když uživatel změní vybranou položku v nadřízeném selectu...
	parentSelect.addEventListener('change', () => {
		// pokud existuje atribut data-items...
		if (items) {
			// nahrajeme rovnou do podřízeného selectboxu nové položky
			updateSelectbox(childSelect, items[parentSelect.value]);
		}

		// pokud existuje atribut data-url...
		if (url) {
			// uděláme AJAXový požadavek na endpoint s vybranou položkou místo placeholderu
			fetch(url.replace(encodeURIComponent('#'), encodeURIComponent(parentSelect.value)))
				.then((response) => response.json())
				// a nahrajeme do podřízeného selectboxu nové položky
				.then((data) => updateSelectbox(childSelect, data));
		}
	});
});

// přepíše <options> v <select>
function updateSelectbox(select, items)
{
	select.innerHTML = ''; // odstraníme vše
	for (let id in items) { // vložíme nové
		let el = document.createElement('option');
		el.setAttribute('value', id);
		el.innerText = items[id];
		select.appendChild(el);
	}
}

Více prvků a znovupoužitelnost

Řešení není limitované dvěma selectboxy, lze vytvořit klidně kaskádu tří nebo více na sobě závisejících prvků. Například doplníme volbu ulice, která bude závislá na zvoleném městě:

$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()) : []);

Také může více selectboxů záviset na jednom společném. Stačí jen analogicky nastavit data- atributy a naplnění položek pomocí setItems().

Přičemž není potřeba dělat žádný zásah do JavaScriptového kódu, který funguje univerzálně.

Bezpečnost

I v těchto ukázkách se stále zachovávají všechny bezpečnostní mechanismy, kterými disponují formuláře v Nette. Zejména že každý selectbox kontroluje, zda vybraná varianta je jednou z nabízených a tedy útočník nemůže podstrčit jinou hodnotu.


Řešení funguje v Nette 2.4 a novějším, ukázky kódu jsou psané pro Nette pro PHP 8. Aby fungovaly ve starších verzích, nahraďte property promotion a fn() za function () use (...) { ... }.

Komentáře

Subscribe
Upozornit na
guest
1 Komentář
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
novak.radek

pokud hledám řešení dynamicky doplňovaných selectboxů.
Když se však zamyslím nad praktickým použitím při zadávání adres ve formulářích, bylo by řešení uživatelsky poněkud nepříjemné. Hned u druhého výběru hrozí, že mi shoří kolečko u myši, nedejbůh že bych měl vybírat ještě z ulic.
Tím ale nechci snižovat úroveň článku, naopak, díky za opravdu polopatický popis!

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.