Dinamik Snippet'ler

Uygulama geliştirirken, örneğin bir tablonun tek tek satırları veya bir listenin öğeleri üzerinde AJAX işlemleri yapma ihtiyacı oldukça sık ortaya çıkar. Örnek olarak, makalelerin bir listesini seçebiliriz, burada her makale için giriş yapmış kullanıcının “beğen/beğenme” derecelendirmesini seçmesine izin veririz. AJAX olmadan presenter ve ilgili şablonun kodu yaklaşık olarak aşağıdaki gibi görünecektir (en önemli bölümleri listeliyorum, kod derecelendirmeleri işaretlemek ve makale koleksiyonunu almak için bir servisin varlığını varsayar – belirli uygulama bu kılavuzun amaçları için önemli değildir):

public function handleLike(int $articleId): void
{
	$this->ratingService->saveLike($articleId, $this->user->id);
	$this->redirect('this');
}

public function handleUnlike(int $articleId): void
{
	$this->ratingService->removeLike($articleId, $this->user->id);
	$this->redirect('this');
}

Şablon:

<article n:foreach="$articles as $article">
	<h2>{$article->title}</h2>
	<div class="content">{$article->content}</div>
	{if !$article->liked}
		<a n:href="like! $article->id" class=ajax>beğen</a>
	{else}
		<a n:href="unlike! $article->id" class=ajax>artık beğenmiyorum</a>
	{/if}
</article>

AJAXlaştırma

Şimdi bu basit uygulamayı AJAX ile donatalım. Bir makalenin derecelendirmesini değiştirmek, bir yönlendirme gerektirecek kadar önemli değildir ve bu nedenle ideal olarak arka planda AJAX ile gerçekleşmelidir. Eklentilerden yardımcı betiği AJAX bağlantılarının ajax CSS sınıfına sahip olduğu olağan kuralıyla kullanacağız.

Ancak, bunu tam olarak nasıl yapacağız? Nette 2 yol sunar: dinamik snippet'ler yolu ve bileşenler yolu. Her ikisinin de artıları ve eksileri vardır, bu yüzden onları birer birer göstereceğiz.

Dinamik Snippet Yolu

Latte terminolojisinde dinamik bir snippet, snippet adında bir değişkenin kullanıldığı {snippet} makrosunun özel bir kullanım durumunu ifade eder. Böyle bir snippet şablonda herhangi bir yerde bulunamaz – statik bir snippet, yani sıradan bir snippet veya {snippetArea} içinde sarmalanmalıdır. Şablonumuzu aşağıdaki gibi değiştirebiliriz.

{snippet articlesContainer}
	<article n:foreach="$articles as $article">
		<h2>{$article->title}</h2>
		<div class="content">{$article->content}</div>
		{snippet article-{$article->id}}
			{if !$article->liked}
				<a n:href="like! $article->id" class=ajax>beğen</a>
			{else}
				<a n:href="unlike! $article->id" class=ajax>artık beğenmiyorum</a>
			{/if}
		{/snippet}
	</article>
{/snippet}

Her makale şimdi adında makale ID'si bulunan bir snippet tanımlar. Tüm bu snippet'ler daha sonra articlesContainer adlı tek bir snippet ile birlikte sarmalanır. Bu sarmalayıcı snippet'i atlarsak, Latte bizi bir istisna ile uyaracaktır.

Geriye presenter'a yeniden çizimi eklemek kalıyor – sadece statik sarmalayıcıyı yeniden çizmek yeterlidir.

public function handleLike(int $articleId): void
{
	$this->ratingService->saveLike($articleId, $this->user->id);
	if ($this->isAjax()) {
		$this->redrawControl('articlesContainer');
		// $this->redrawControl('article-' . $articleId); -- gerekli değil
	} else {
		$this->redirect('this');
	}
}

Benzer şekilde, kardeş metot handleUnlike()'ı da değiştiririz ve AJAX işlevseldir!

Ancak çözümün bir dezavantajı var. AJAX isteğinin nasıl ilerlediğini daha fazla incelersek, uygulamanın dışarıdan verimli görünmesine rağmen (belirli makale için yalnızca tek bir snippet döndürür), aslında sunucuda tüm snippet'leri oluşturduğunu fark ederiz. İstenen snippet'i payload'a yerleştirdi ve diğerlerini attı (bu nedenle onları veritabanından tamamen gereksiz yere aldı).

Bu süreci optimize etmek için, $articles koleksiyonunu şablona ilettiğimiz yere müdahale etmemiz gerekecek (diyelim ki renderDefault() metodunda). Sinyal işlemenin render<Something> metotlarından önce gerçekleştiği gerçeğinden yararlanacağız:

public function handleLike(int $articleId): void
{
	// ...
	if ($this->isAjax()) {
		// ...
		$this->template->articles = [
			$this->db->table('articles')->get($articleId),
		];
	} else {
		// ...
}

public function renderDefault(): void
{
	if (!isset($this->template->articles)) {
		$this->template->articles = $this->db->table('articles');
	}
}

Şimdi, sinyal işlenirken, tüm makaleleri içeren koleksiyon yerine şablona yalnızca tek bir makale içeren bir dizi iletilir – yani, tarayıcıya payload'da oluşturmak ve göndermek istediğimiz makale. {foreach} bu nedenle yalnızca bir kez çalışır ve fazladan snippet oluşturulmaz.

Bileşen Yolu

Tamamen farklı bir çözüm yaklaşımı dinamik snippet'lerden kaçınır. Hile, tüm mantığı özel bir bileşene aktarmaktır – bundan sonra derecelendirme girişi presenter tarafından değil, özel bir LikeControl tarafından yönetilecektir. Sınıf aşağıdaki gibi görünecektir (ayrıca render, handleUnlike vb. metotları da içerecektir):

class LikeControl extends Nette\Application\UI\Control
{
	public function __construct(
		private Article $article,
	) {
	}

	public function handleLike(): void
	{
		$this->ratingService->saveLike($this->article->id, $this->presenter->user->id);
		if ($this->presenter->isAjax()) {
			$this->redrawControl();
		} else {
			$this->presenter->redirect('this');
		}
	}
}

Bileşen şablonu:

{snippet}
	{if !$article->liked}
		<a n:href="like!" class=ajax>beğen</a>
	{else}
		<a n:href="unlike!" class=ajax>artık beğenmiyorum</a>
	{/if}
{/snippet}

Tabii ki, görünüm şablonumuz değişecek ve presenter'a bir fabrika eklememiz gerekecek. Bileşeni veritabanından aldığımız makale sayısı kadar oluşturacağımız için, onu “çoğaltmak” için Multiplier sınıfını kullanacağız.

protected function createComponentLikeControl()
{
	$articles = $this->db->table('articles');
	return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) {
		return new LikeControl($articles[$articleId]);
	});
}

Görünüm şablonu gerekli minimuma indirildi (ve tamamen snippet'lerden arındırıldı!):

<article n:foreach="$articles as $article">
	<h2>{$article->title}</h2>
	<div class="content">{$article->content}</div>
	{control "likeControl-$article->id"}
</article>

Neredeyse bitti: uygulama artık AJAX ile çalışacak. Burada da uygulamayı optimize etmemiz gerekiyor, çünkü Nette Database kullanımı nedeniyle, sinyal işlenirken veritabanından tüm makaleler gereksiz yere yüklenir, oysa sadece bir tanesi yeterlidir. Ancak avantajı, bunların oluşturulmamasıdır, çünkü gerçekten sadece bizim bileşenimiz oluşturulur.