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
{
}