Nette ve Saf JavaScript ile Zarif Bağımlı Seçim Kutuları

3 yıl önce Yazan David Grudl  

Birindeki değeri seçtikten sonra diğerine dinamik olarak seçeneklerin yüklendiği bağlantılı seçim kutuları nasıl oluşturulur? Nette ve saf JavaScript ile bu kolay bir görevdir. Temiz, yeniden kullanılabilir ve güvenli bir çözüm göstereceğiz.

Veri Modeli

Örnek olarak, ülke ve şehir seçimi için seçim kutuları içeren bir form oluşturacağız.

Öncelikle, her iki seçim kutusu için öğeleri döndürecek veri modelini hazırlayalım. Muhtemelen bunları veritabanından alacaktır. Kesin uygulama önemli değil, bu yüzden arayüzün nasıl görüneceğini sadece ima edeceğiz:

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

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

Toplam şehir sayısı gerçekten büyük olduğundan, bunları AJAX kullanarak alacağız. Bu amaçla, bize ayrı ülkelerdeki şehirleri JSON olarak döndürecek bir EndpointPresenter, yani bir API oluşturacağız:

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);
	}
}

Eğer şehir sayısı az olsaydı (belki başka bir gezegende 😉) veya model basitçe çok fazla olmayan verileri temsil etseydi, hepsini doğrudan bir dizi olarak JavaScript'e iletebilir ve AJAX isteklerinden tasarruf edebilirdik. Bu durumda EndpointPresenter'a gerek kalmazdı.

Form

Ve formun kendisine geçelim. İki seçim kutusu oluşturacağız ve bunları birbirine bağlayacağız, yani üst öğenin (country) seçilen değerine bağlı olarak alt (city) öğeleri ayarlayacağız. Önemli olan, bunu onAnchor olay işleyicisinde, yani formun kullanıcı tarafından gönderilen değerleri zaten bildiği anda yapmamızdır.

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

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

		$city = $form->addSelect('city', 'Şehir:');
		// <-- buraya daha sonra bir şeyler ekleyeceğiz

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

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

Bu şekilde oluşturulan form, JavaScript olmadan da çalışacaktır. Şöyle ki, kullanıcı önce ülkeyi seçer, formu gönderir, ardından şehir teklifi görünür, birini seçer ve formu tekrar gönderir.

Ancak bizi ilgilendiren, JavaScript kullanarak şehirlerin dinamik olarak yüklenmesidir. Buna yaklaşmanın en temiz yolu, hangi seçim kutularının bağlantılı olduğu ve verilerin nereden alınması gerektiği bilgisini HTML'ye (ve dolayısıyla JS'ye) göndereceğimiz data- niteliklerini kullanmaktır.

Her alt seçim kutusuna, üst öğenin adını içeren data-depends niteliğini ve ayrıca ya AJAX kullanarak öğeleri alması gereken URL'yi içeren data-url'yi ya da tüm varyantları doğrudan listelediğimiz data-items'ı ileteceğiz.

AJAX varyantıyla başlayalım. Üst öğenin adını country ve Endpoint:cities'e bir bağlantı ileteceğiz. # karakterini bir yer tutucu olarak kullanıyoruz ve JavaScript bunun yerine kullanıcı tarafından seçilen anahtarı ekleyecektir.

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

Peki AJAX'sız varyant? Tüm ülkelerin ve tüm şehirlerinin bir dizisini hazırlayacağız ve bunu data-items niteliğine ileteceğiz:

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

$city = $form->addSelect('city', 'Şehir:')
	->setHtmlAttribute('data-depends', $country->getHtmlName())
	->setHtmlAttribute('data-items', $items);

Ve geriye sadece JavaScript işleyicisini yazmak kalıyor.

JavaScript İşleyicisi

Aşağıdaki kod evrenseldir, örnekteki belirli country ve city seçim kutularına bağlı değildir, ancak sayfadaki herhangi bir seçim kutusunu bağlar, sadece belirtilen data- niteliklerini ayarlamak yeterlidir.

Kod saf vanilla JS ile yazılmıştır, bu nedenle jQuery veya başka bir kütüphane gerektirmez.

// sayfadaki tüm alt seçim kutularını buluruz
document.querySelectorAll('select[data-depends]').forEach((childSelect) => {
	let parentSelect = childSelect.form[childSelect.dataset.depends]; // üst <select>
	let url = childSelect.dataset.url; // data-url niteliği
	let items = JSON.parse(childSelect.dataset.items || 'null'); // data-items niteliği

	// kullanıcı üst select'teki seçili öğeyi değiştirdiğinde...
	parentSelect.addEventListener('change', () => {
		// eğer data-items niteliği varsa...
		if (items) {
			// doğrudan alt seçim kutusuna yeni öğeleri yükleriz
			updateSelectbox(childSelect, items[parentSelect.value]);
		}

		// eğer data-url niteliği varsa...
		if (url) {
			// yer tutucu yerine seçili öğe ile endpoint'e AJAX isteği yaparız
			fetch(url.replace(encodeURIComponent('#'), encodeURIComponent(parentSelect.value)))
				.then((response) => response.json())
				// ve alt seçim kutusuna yeni öğeleri yükleriz
				.then((data) => updateSelectbox(childSelect, data));
		}
	});
});

// <select> içindeki <options>'ları yeniden yazar
function updateSelectbox(select, items)
{
	select.innerHTML = ''; // her şeyi kaldırırız
	for (let id in items) { // yenilerini ekleriz
		let el = document.createElement('option');
		el.setAttribute('value', id);
		el.innerText = items[id];
		select.appendChild(el);
	}
}

Daha Fazla Öğe ve Yeniden Kullanılabilirlik

Çözüm iki seçim kutusuyla sınırlı değildir, üç veya daha fazla birbirine bağlı öğeden oluşan bir kademe oluşturmak mümkündür. Örneğin, seçilen şehre bağlı olacak sokak seçimini ekleyelim:

$street = $form->addSelect('street', 'Sokak:')
	->setHtmlAttribute('data-depends', $city->getHtmlName())
	->setHtmlAttribute('data-url', $this->link('Endpoint:streets', '#'));

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

Ayrıca, birden fazla seçim kutusu tek bir ortak olana bağlı olabilir. Sadece data- niteliklerini benzer şekilde ayarlamak ve setItems() kullanarak öğeleri doldurmak yeterlidir.

Bu arada, evrensel olarak çalışan JavaScript kodunda herhangi bir müdahale yapmaya gerek yoktur.

Güvenlik

Bu örneklerde bile, Nette'deki formların sahip olduğu tüm güvenlik mekanizmaları hala korunmaktadır. Özellikle, her seçim kutusu seçilen varyantın sunulanlardan biri olup olmadığını kontrol eder ve böylece saldırgan başka bir değer gönderemez.


Çözüm Nette 2.4 ve sonrasında çalışır, kod örnekleri PHP 8 için yazılmıştır. Eski sürümlerde çalışması için property promotion ve fn() yerine function () use (...) { ... } yazın.