Зависими селектиращи полета елегантно в Nette и чист JS
Как да създадем верижни кутии за избор, в които след избор на стойност в едната опциите се актуализират динамично в другата? Това е лесна задача в Nette и чист JavaScript. Ще покажем решение, което е чисто, многократно използваемо и сигурно.
Модел на данните
Като пример, нека създадем формуляр, съдържащ полета за избор на държава и град.
Първо ще подготвим модел на данни, който ще връща записи за двете полета за избор. Той вероятно ще ги извлича от базата данни. Точната имплементация не е от съществено значение, затова нека само подскажем как ще изглежда интерфейсът:
class World
{
public function getCountries(): array
{
return ...
}
public function getCities($country): array
{
return ...
}
}
Тъй като общият брой на градовете е много
голям, ще ги извличаме с помощта на AJAX. За
тази цел ще създадем EndpointPresenter
, API,
който ще връща градовете във всяка държава
като 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);
}
}
Ако има малко градове (например на друга
планета 😉) или ако моделът представя данни,
които просто не са много, можем да ги
предадем всички като масив на JavaScript и да
спестим AJAX заявките. В този случай няма да
има нужда от EndpointPresenter
.
Форма
И нека преминем към самия формуляр. Ще
създадем две полета за избор и ще ги
свържем, т.е. ще зададем елементите на
детето (city
) в зависимост от избраната
стойност на родителя (country
). Важното
е, че правим това в обработчика на събитието
onAnchor,
т.е. в момента, в който формата вече знае
стойностите, подадени от потребителя.
class DemoPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private World $world,
) {}
protected function createComponentForm(): Form
{
$form = new Form;
$country = $form->addSelect('country', 'Country:', $this->world->getCountries())
->setPrompt('----');
$city = $form->addSelect('city', 'City:');
// <-- ще добавим още нещо тук
$form->onAnchor[] = fn() =>
$city->setItems($country->getValue()
? $this->world->getCities($country->getValue())
: []);
// $form->onSuccess[] = ...
return $form;
}
}
Създадената по този начин форма ще работи без JavaScript. Това става, като потребителят първо избере държава, изпрати формуляра, след което ще се появи меню с градове, избере един от тях и отново изпрати формуляра.
Ние обаче се интересуваме от динамично
зареждане на градове с помощта на JavaScript.
Най-чистият начин да подходим към това е да
използваме атрибути data-
, при които
изпращаме информация на HTML (и следователно
на JS) за това кои полета за избор са свързани
и откъде да се извличат данни.
За всяко дъщерно поле за избор предаваме
атрибут data-depends
с името на
родителския елемент, а след това или
атрибут data-url
с URL адреса, откъдето да
изтеглим елементите с помощта на AJAX, или
атрибут data-items
, в който директно
изброяваме всички опции.
Нека да започнем с варианта AJAX. Предаваме
името на родителския елемент country
и
препратка към Endpoint:cities
. Използваме
символа #
като заместител и JavaScript ще
постави вместо него избрания от
потребителя ключ.
$city = $form->addSelect('city', 'City:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-url', $this->link('Endpoint:cities', '#'));
А вариантът без AJAX? Подготвяме масив от
всички държави и всички техни градове,
който предаваме на атрибута data-items
:
$items = [];
foreach ($this->world->getCountries() as $id => $name) {
$items[$id] = $this->world->getCities($id);
}
$city = $form->addSelect('city', 'City:')
->setHtmlAttribute('data-depends', $country->getHtmlName())
->setHtmlAttribute('data-items', $items);
Обработваща програма на JavaScript
Следващият код е универсален, той не е
обвързан с конкретните полета за избор
country
и city
от примера, а ще
свърже всички полета за избор на
страницата, като просто зададете
споменатите атрибути data-
.
Кодът е написан на чист vanilla JS, така че не изисква jQuery или друга библиотека.
// намиране на всички детски полета за избор в страницата
document.querySelectorAll('select[data-depends]').forEach((childSelect) => {
let parentSelect = childSelect.form[childSelect.dataset.depends]; // родител <select>
let url = childSelect.dataset.url; // атрибут data-url
let items = JSON.parse(childSelect.dataset.items || 'null'); // атрибут data-items
// когато потребителят промени избрания елемент в родителската селекция...
parentSelect.addEventListener('change', () => {
// ако атрибутът data-items съществува...
if (items) {
// зареждане на нови елементи директно в детската селекция
updateSelectbox(childSelect, items[parentSelect.value]);
}
// ако атрибутът data-url съществува...
if (url) {
// правим AJAX заявка към крайната точка с избрания елемент вместо със заместител
fetch(url.replace(encodeURIComponent('#'), encodeURIComponent(parentSelect.value)))
.then((response) => response.json())
// и зареждаме нови елементи в допълнителния селектор
.then((data) => updateSelectbox(childSelect, data));
}
});
});
// замества <options> в <select>
function updateSelectbox(select, items)
{
select.innerHTML = ''; // remove all
for (let id in items) { // вмъкване на нови
let el = document.createElement('option');
el.setAttribute('value', id);
el.innerText = items[id];
select.appendChild(el);
}
}
Повече елементи и възможност за повторна употреба
Решението не е ограничено до две полета за избор, можете да създадете каскада от три или повече зависими елемента. Например, добавяме избор на улица, който зависи от избрания град:
$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()) : []);
Също така, няколко полета за избор могат
да зависят от едно общо такова. Просто
задайте атрибутите data-
по аналогия и
попълнете елементите с setItems()
.
Няма абсолютно никаква нужда да правите модификация на кода на JavaScript, който работи универсално.
Сигурност
Дори и в тези примери всички механизми за сигурност, с които разполагат формите на Nette, са запазени. По-специално, всяко поле за избор проверява дали избраната опция е една от предложените и по този начин нападателят не може да подмени различна стойност.
Решението работи в Nette 2.4 и по-нови
версии, а примерните кодове са написани за
PHP 8. За да работят в по-стари версии,
заменете property
promotion и fn()
с
function () use (...) { ... }
.
За да изпратите коментар, моля, влезте в системата