AJAX ve Snippet'ler

Günümüzde modern web uygulamalarının yarısı sunucuda, yarısı da tarayıcıda çalışmaktadır. AJAX hayati bir birleştirici faktördür. Nette Framework ne gibi destekler sunuyor?

  • Şablon parçaları gönderme (snippet olarak adlandırılır)
  • PHP ve JavaScript arasında değişken aktarımı
  • AJAX uygulamaları hata ayıklama

AJAX İsteği

Bir AJAX isteği klasik bir istekten farklı değildir – sunum yapan kişi belirli bir görünüm ve parametrelerle çağrılır. Buna nasıl yanıt verileceği de sunucuya bağlıdır: bir HTML kod parçası (HTML snippet), bir XML belgesi, bir JSON nesnesi veya JavaScript kodu döndüren kendi rutinini kullanabilir.

Sunucu tarafında, bir AJAX isteği $httpRequest->isAjax() HTTP isteğini kapsülleyen hizmet yöntemi kullanılarak algılanabilir ( X-Requested-With HTTP başlığına dayalı olarak algılar). Sunucunun içinde, $this->isAjax() yöntemi şeklinde bir kısayol mevcuttur.

JSON'da tarayıcıya veri göndermeye adanmış payload adında önceden işlenmiş bir nesne vardır.

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

JSON çıktınız üzerinde tam kontrol sahibi olmak için sunumunuzda sendJson yöntemini kullanın. Sunucuyu hemen sonlandırır ve şablon olmadan yaparsınız:

$this->sendJson(['key' => 'value', /* ... */]);

HTML göndermek istiyorsak, AJAX istekleri için özel bir şablon ayarlayabiliriz:

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

Naja

Naja kütüphanesi, tarayıcı tarafında AJAX isteklerini işlemek için kullanılır. Bir node.js paketi olarak yükleyin (Webpack, Rollup, Vite, Parcel ve daha fazlası ile kullanmak için):

npm install naja

…veya doğrudan sayfa şablonuna ekleyin:

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

Normal bir bağlantıdan (sinyal) veya form gönderiminden bir AJAX isteği oluşturmak için ilgili bağlantıyı, formu veya düğmeyi ajax sınıfıyla işaretlemeniz yeterlidir:

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

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

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

Parçacıklar

Yerleşik AJAX desteğinin çok daha güçlü bir aracı vardır – snippet'ler. Bunları kullanmak, normal bir uygulamayı yalnızca birkaç satır kod kullanarak bir AJAX uygulamasına dönüştürmeyi mümkün kılar. Tüm bunların nasıl çalıştığı, koduna derlemede veya GitHub'da da erişilebilen Fifteen örneğinde gösterilmiştir.

Snippet'lerin çalışma şekli, ilk (yani AJAX olmayan) istek sırasında tüm sayfanın aktarılması ve ardından her AJAX alt isteğinde (aynı sunucunun aynı görünümünün isteği) yalnızca değiştirilen parçaların kodunun daha önce bahsedilen payload deposuna aktarılmasıdır.

Snippet'ler size Ruby on Rails için Hotwire veya Symfony UX Turbo'yu hatırlatabilir, ancak Nette bunları on dört yıl önce buldu.

Snippet'lerin Geçersiz Kılınması

Control sınıfının her bir torunu (ki bir Presenter da öyledir), bir istek sırasında yeniden oluşturmasını gerektiren herhangi bir değişiklik olup olmadığını hatırlayabilir. Bunu işlemek için bir çift yöntem vardır: redrawControl() ve isControlInvalid(). Bir örnek:

public function handleLogin(string $user): void
{
	// Nesne, kullanıcı giriş yaptıktan sonra yeniden oluşturulmalıdır
	$this->redrawControl();
	// ...
}

Ancak Nette, tüm bileşenlerden daha da ince bir çözünürlük sunar. Listelenen yöntemler, isteğe bağlı bir parametre olarak “snippet” olarak adlandırılan bir ismi kabul eder. Bir “snippet” temel olarak şablonunuzdaki bir Latte makrosu tarafından bu amaç için işaretlenmiş bir öğedir, daha sonra bu konuda daha fazla bilgi verilecektir. Böylece bir bileşenden şablonunun sadece parçalarını yeniden çizmesini istemek mümkündür. Bileşenin tamamı geçersiz kılınırsa, tüm parçacıkları yeniden oluşturulur. Bir bileşen, alt bileşenlerinden herhangi birinin geçersiz olması durumunda da “geçersiz” olur.

$this->isControlInvalid(); // -> false

$this->redrawControl('header'); // 'header' adlı parçacığı geçersiz kılar
$this->isControlInvalid('header'); // -> true
$this->isControlInvalid('footer'); // -> false
$this->isControlInvalid(); // -> true, en az bir snippet geçersiz

$this->redrawControl(); // tüm bileşeni, her parçacığı geçersiz kılar
$this->isControlInvalid('footer'); // -> true

Sinyal alan bir bileşen otomatik olarak yeniden çizilmek üzere işaretlenir.

Snippet yeniden çizimi sayesinde hangi öğelerin hangi kısımlarının yeniden işlenmesi gerektiğini tam olarak biliyoruz.

Etiket {snippet} … {/snippet}

Sayfanın oluşturulması normal bir istekle çok benzer şekilde ilerler: aynı şablonlar yüklenir, vb. Ancak önemli olan, çıktıya ulaşmaması gereken kısımların dışarıda bırakılmasıdır; diğer kısımlar bir tanımlayıcı ile ilişkilendirilmeli ve bir JavaScript işleyicisi için anlaşılabilir bir biçimde kullanıcıya gönderilmelidir.

Sözdizimi

Şablonda bir kontrol veya snippet varsa, bunu {snippet} ... {/snippet} pair etiketini kullanarak sarmalıyız – bu, işlenen snippet'in “kesilip çıkarılmasını” ve tarayıcıya gönderilmesini sağlayacaktır. Ayrıca onu bir yardımcı içine alacaktır <div> etiketi (farklı bir etiket kullanmak mümkündür). Aşağıdaki örnekte header adında bir snippet tanımlanmıştır. Bir bileşenin şablonunu da temsil edebilir:

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

dışındaki bir türden bir snippet <div> veya ek HTML öznitelikleri içeren bir snippet, öznitelik varyantı kullanılarak elde edilir:

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

Dinamik Parçacıklar

Nette'de ayrıca bir çalışma zamanı parametresine dayalı olarak dinamik bir adla snippet'ler tanımlayabilirsiniz. Bu, sadece bir satırı değiştirmemiz gereken ancak tüm listeyi onunla birlikte aktarmak istemediğimiz çeşitli listeler için en uygun olanıdır. Bunun bir örneği şöyle olabilir:

<ul n:snippet="itemsContainer">
	{foreach $list as $id => $item}
		<li n:snippet="item-$id">{$item} <a class="ajax" n:href="update! $id">update</a></li>
	{/foreach}
</ul>

Birkaç dinamik snippet içeren itemsContainer adında bir statik snippet vardır: item-0, item-1 ve benzeri.

Dinamik bir snippet'i doğrudan yeniden çizemezsiniz ( item-1 adresinin yeniden çizilmesinin hiçbir etkisi yoktur), üst snippet'ini yeniden çizmeniz gerekir (bu örnekte itemsContainer). Bu, üst snippet'in kodunun yürütülmesine neden olur, ancak daha sonra tarayıcıya yalnızca alt snippet'leri gönderilir. Alt snippet'lerden yalnızca birini göndermek istiyorsanız, diğer alt snippet'leri oluşturmamak için ana snippet'in girdisini değiştirmeniz gerekir.

Yukarıdaki örnekte, bir AJAX isteği için $list dizisine yalnızca bir öğe ekleneceğinden emin olmanız gerekir, bu nedenle foreach döngüsü yalnızca bir dinamik parçacık yazdıracaktır.

class HomePresenter extends Nette\Application\UI\Presenter
{
	/**
	 * This method returns data for the list.
	 * Usually this would just request the data from a model.
	 * For the purpose of this example, the data is hard-coded.
	 */
	private function getTheWholeList(): array
	{
		return [
			'First',
			'Second',
			'Third',
		];
	}

	public function renderDefault(): void
	{
		if (!isset($this->template->list)) {
			$this->template->list = $this->getTheWholeList();
		}
	}

	public function handleUpdate(int $id): void
	{
		$this->template->list = $this->isAjax()
				? []
				: $this->getTheWholeList();
		$this->template->list[$id] = 'Updated item';
		$this->redrawControl('itemsContainer');
	}
}

Dahil Edilen Bir Şablondaki Parçacıklar

Snippet, farklı bir şablondan dahil edilen bir şablonda olabilir. Bu durumda, ikinci şablondaki dahil etme kodunu snippetArea makrosuyla sarmamız gerekir, ardından hem snippetArea'yı hem de gerçek snippet'i yeniden çizeriz.

Makro snippetArea, içindeki kodun yürütülmesini sağlar, ancak tarayıcıya yalnızca dahil edilen şablondaki gerçek parçacık gönderilir.

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

Ayrıca dinamik snippet'lerle de birleştirebilirsiniz.

Ekleme ve Silme

Listeye yeni bir öğe ekler ve itemsContainer adresini geçersiz kılarsanız, AJAX isteği yenisini de içeren parçacıkları döndürür, ancak javascript işleyicisi bunu işleyemez. Bunun nedeni, yeni oluşturulan ID'ye sahip bir HTML öğesi olmamasıdır.

Bu durumda, en basit yol tüm listeyi bir parçacığa daha sarmak ve hepsini geçersiz kılmaktır:

{snippet wholeList}
<ul n:snippet="itemsContainer">
	{foreach $list as $id => $item}
	<li n:snippet="item-$id">{$item} <a class="ajax" n:href="update! $id">update</a></li>
	{/foreach}
</ul>
{/snippet}
<a class="ajax" n:href="add!">Add</a>
public function handleAdd(): void
{
	$this->template->list = $this->getTheWholeList();
	$this->template->list[] = 'New one';
	$this->redrawControl('wholeList');
}

Aynı şey bir öğeyi silmek için de geçerlidir. Boş snippet göndermek mümkün olabilir, ancak genellikle listeler sayfalandırılabilir ve bir öğeyi silip diğerini (sayfalandırılmış listenin farklı bir sayfasında bulunan) yüklemek karmaşık olacaktır.

Bileşene Parametre Gönderme

AJAX isteği aracılığıyla bileşene parametreler gönderdiğimizde, ister sinyal parametreleri ister kalıcı parametreler olsun, bileşenin adını da içeren global adlarını sağlamalıyız. Parametrenin tam adı getParameterId() yöntemini döndürür.

$.getJSON(
	{link changeCountBasket!},
	{
		{$control->getParameterId('id')}: id,
		{$control->getParameterId('count')}: count
	}
});

Ve bileşende karşılık gelen parametrelerle yöntemi işleyin.

public function handleChangeCountBasket(int $id, int $count): void
{

}
versiyon: 4.0