AJAX ve snippet'ler

İşlevselliğin çoğu zaman sunucu ile tarayıcı arasında dağıtıldığı modern web uygulamaları çağında AJAX, vazgeçilmez bir bağlayıcı unsurdur. Nette Framework bu alanda hangi olanakları sunuyor?

  • snippet denilen şablon parçalarının gönderilmesi
  • PHP ile JavaScript arasında değişken aktarımı
  • AJAX isteklerini hata ayıklama araçları

AJAX isteği

Bir AJAX isteği, klasik bir HTTP isteğinden temelde farklı değildir. Belirli parametrelerle bir presenter çağrılır. İsteğe nasıl yanıt vereceğine presenter karar verir; veriyi JSON formatında döndürebilir, bir HTML kodu parçası, bir XML belgesi vb. gönderebilir.

Tarayıcı tarafında AJAX isteğini fetch() fonksiyonuyla başlatırız:

fetch(url, {
	headers: {'X-Requested-With': 'XMLHttpRequest'},
})
.then(response => response.json())
.then(payload => {
	// yanıtı işle
});

Sunucu tarafında AJAX isteği, HTTP isteğini kapsülleyen servisin $httpRequest->isAjax() metoduyla tanınır. Algılama için X-Requested-With HTTP header'ını kullanır, bu yüzden onu göndermek çok önemlidir. Presenter içinde $this->isAjax() metodunu kullanabilirsiniz.

Veriyi JSON formatında göndermek isterseniz sendJson() metodunu kullanın. Bu metot presenter'ın çalışmasını da sonlandırır.

public function actionExport(): void
{
	$this->sendJson($this->model->getData());
}

AJAX için tasarlanmış özel bir şablonla yanıt vermeyi planlıyorsanız şöyle yapabilirsiniz:

public function handleClick($param): void
{
	if ($this->isAjax()) {
		$this->template->setFile('path/to/ajax.latte');
	}
	// ...
}

Snippet'ler

Nette'in sunucuyu istemciyle birleştirmek için sunduğu en güçlü araç snippet'lerdir. Onlarla sıradan bir uygulamayı en az çabayla ve yalnızca birkaç satır kodla AJAX uygulamasına dönüştürebilirsiniz. Bunun nasıl çalıştığını Fifteen örneği gösterir; kodunu GitHub üzerinde bulabilirsiniz.

Snippet'ler, tüm sayfayı yeniden yüklemek yerine yalnızca sayfanın bölümlerini güncellemenizi sağlar. Bu yalnızca daha hızlı ve verimli olmakla kalmaz, aynı zamanda daha rahat bir kullanıcı deneyimi sunar. Snippet'ler size Ruby on Rails için Hotwire'ı veya Symfony UX Turbo'yu hatırlatabilir. İlginçtir ki Nette snippet'leri 14 yıl önce tanıttı.

Snippet'ler nasıl çalışır? Sayfa ilk yüklendiğinde (AJAX olmayan istek) tüm sayfa, tüm snippet'ler dahil, yüklenir. Kullanıcı sayfayla etkileşime girdiğinde (örneğin bir düğmeye tıkladığında, bir formu gönderdiğinde vb.), tüm sayfayı yeniden yüklemek yerine bir AJAX isteği başlatılır. Presenter'daki kod eylemi gerçekleştirir ve hangi snippet'lerin güncellenmesi gerektiğine karar verir. Nette bu snippet'leri render eder ve snippet'leri içeren bir dizi taşıyan JSON payload'ı olarak gönderir. Tarayıcıdaki işleyici kod, aldığı snippet'leri sayfaya geri yerleştirir. Böylece yalnızca değişen snippet'lerin kodu aktarılır, bu da tüm sayfa içeriğini aktarmaya kıyasla bant genişliğinden tasarruf sağlar ve yüklemeyi hızlandırır. redrawControl() ile hiçbir snippet geçersiz kılınmazsa, Nette AJAX isteğinde de tüm sayfayı döndürür; snippet'ler yalnızca bir şey geçersiz kılındığında gönderilir.

Naja

Snippet'leri tarayıcı tarafında işlemek için Naja kütüphanesi kullanılır. Onu bir Node.js paketi olarak kurun (Webpack, Rollup, Vite, Parcel gibi paketleyicilerle kullanmak için):

npm install naja

…ya da doğrudan sayfa şablonuna ekleyin:

<script src="https://unpkg.com/naja@3/dist/Naja.min.js"></script>

Önce kütüphaneyi başlatmanız gerekir:

naja.initialize();

Sıradan bir bağlantıyı (sinyali) veya form gönderimini AJAX isteğine dönüştürmek için ilgili bağlantıyı, formu veya düğmeyi ajax sınıfıyla işaretlemeniz yeterlidir:

<a n:href="go!" class="ajax">Git</a>

<form n:name="form" class="ajax">
    <input n:name="submit">
</form>

veya

<form n:name="form">
    <input n:name="submit" class="ajax">
</form>

Snippet'lerin yeniden çizilmesi

Control sınıfının her nesnesi (Presenter'ın kendisi dahil), yeniden çizilmesini gerektiren değişikliklerin olup olmadığını izler. Bunun için redrawControl() metodu kullanılır:

public function handleLogin(string $user): void
{
	// girişten sonra ilgili bölümün yeniden çizilmesi gerekir
	$this->redrawControl();
	// ...
}

Nette, neyin yeniden çizileceği üzerinde daha da ince bir denetim sağlar. Metot, argüman olarak snippet'in adını kabul edebilir. Böylece şablon parçaları düzeyinde geçersiz kılmak (yani yeniden çizilmeye zorlamak) mümkündür. Tüm bileşen geçersiz kılınırsa, içindeki her snippet de yeniden çizilir:

// 'header' snippet'ini geçersiz kılar
$this->redrawControl('header');

Bekleyen bir geçersiz kılmayı ikinci parametre $redraw ile iptal de edebilirsiniz: $this->redrawControl('header', redraw: false) çağrısı, snippet'i yeniden çizilmesi gerekmeyen olarak işaretler. Tam imza redrawControl(?string $snippet = null, bool $redraw = true) şeklindedir.

Latte'de snippet'ler

Latte'de snippet kullanmak son derece kolaydır. Şablonun bir bölümünü snippet olarak tanımlamak için onu {snippet} ve {/snippet} etiketleriyle sarmanız yeterlidir:

{snippet header}
	<h1>Merhaba ... </h1>
{/snippet}

Snippet, HTML sayfasında özel olarak üretilmiş bir id'ye sahip bir <div> elemanı oluşturur. Snippet yeniden çizildiğinde bu elemanın içeriği güncellenir. Bu yüzden sayfa ilk render edildiğinde tüm snippet'lerin de render edilmesi gerekir, başlangıçta boş olabilseler bile.

Snippet'i <div> dışında bir elemanla, n:niteliği kullanarak da oluşturabilirsiniz:

<article n:snippet="header" class="foo bar">
	<h1>Merhaba ... </h1>
</article>

Snippet alanları

Snippet adları ifade de olabilir:

{foreach $items as $id => $item}
	<li n:snippet="item-{$id}">{$item}</li>
{/foreach}

Tek başına bu, işlevsiz bir ara adımdır: statik bir {snippet} veya {snippetArea} dışında render edilen dinamik bir snippet, Dynamic snippets are allowed only inside static snippet/snippetArea. mesajıyla E_USER_WARNING tetikler. Bunu aşağıda düzeltiyoruz.

Bu, item-0, item-1 gibi birkaç snippet oluşturur. Dinamik bir snippet'i doğrudan geçersiz kılsaydık (örneğin item-1), hiçbir şey yeniden çizilmezdi. Bunun nedeni, snippet'lerin gerçekten alıntı gibi çalışması ve yalnızca kendilerinin doğrudan render edilmesidir. Oysa şablonda teknik olarak item-1 adlı bir snippet yoktur. O ancak snippet'i çevreleyen kod, yani foreach döngüsü, çalıştırıldığında var olur. Bu yüzden şablonun çalıştırılması gereken bölümünü {snippetArea} etiketiyle işaretleriz:

<ul n:snippetArea="itemsContainer">
	{foreach $items as $id => $item}
		<li n:snippet="item-{$id}">{$item}</li>
	{/foreach}
</ul>

Ve hem tek tek snippet'in hem de tüm üst alanın yeniden çizilmesini isteriz:

$this->redrawControl('itemsContainer');
$this->redrawControl('item-1');

Aynı zamanda $items dizisinin yalnızca yeniden çizilmesi gereken öğeleri içermesini sağlamak yerinde olur.

Ana şablona snippet içeren başka bir şablonu {include} etiketiyle dahil edersek, şablon dahil etmeyi yine bir snippetArea içine sarmak ve onu snippet'le birlikte geçersiz kılmak gerekir:

{snippetArea include}
	{include 'included.latte'}
{/snippetArea}
{* included.latte *}
{snippet item}
	...
{/snippet}
$this->redrawControl('include');
$this->redrawControl('item');

Bileşenlerde snippet'ler

Bileşenlerin içinde snippet oluşturabilirsiniz ve Nette bunları otomatik olarak yeniden çizer. Ancak bir sınırlama vardır: snippet'leri yeniden çizmek için Nette render() metodunu parametresiz çağırır. Bu yüzden şablonda parametre aktarmak işe yaramaz:

Tamam
{control productGrid}

çalışmaz:
{control productGrid $arg, $arg}
{control productGrid:paginator}

Kendi verilerinizi gönderme

Snippet'lerle birlikte istemciye istediğiniz ek veriyi gönderebilirsiniz. Onları payload nesnesine yazmanız yeterlidir:

public function actionDelete(int $id): void
{
	// ...
	if ($this->isAjax()) {
		$this->payload->message = 'Başarılı';
	}
}

Yeniden yönlendirme

AJAX isteği sırasında redirect() ve redirectUrl() metotları HTTP yeniden yönlendirmesi göndermez. Bunun yerine hedef URL'yi payload'a (AJAX yanıtında gönderilen veri nesnesine), yani onun payload.redirect özelliğine yazar ve gönderir; asıl yönlendirmeyi ise istemci tarafındaki kütüphane (Naja) gerçekleştirir.

Parametre aktarımı

Bir bileşene AJAX isteğiyle parametre gönderirken, ister sinyal parametreleri ister kalıcı parametreler olsun, istekte bileşenin adını da içeren genel adlarını belirtmemiz gerekir. getParameterId() metodu parametrenin tam adını döndürür.

let url = new URL({link //foo!});
url.searchParams.set({$control->getParameterId('bar')}, bar);

fetch(url, {
	headers: {'X-Requested-With': 'XMLHttpRequest'},
})

Ve bileşende ilgili parametrelere sahip handle metodu:

public function handleFoo(int $bar): void
{
}

İleri okuma

versiyon: 4.x