動的スニペット

アプリケーション開発において、テーブルの個々の行やリストの項目などに対してAJAX操作を実行する必要性がしばしば生じます。例として、記事のリストを表示し、ログインしたユーザーが各記事に対して「いいね/いいねしない」の評価を選択できるようにします。AJAXなしのPresenterと対応するテンプレートのコードは、おおよそ次のようになります(最も重要な部分を示します。コードは評価を記録し、記事のコレクションを取得するためのサービスの存在を前提としています – 具体的な実装はこのチュートリアルの目的には重要ではありません):

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

テンプレート:

<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>いいね</a>
	{else}
		<a n:href="unlike! $article->id" class=ajax>いいねを取り消す</a>
	{/if}
</article>

Ajax化

では、この簡単なアプリケーションにAJAXを追加しましょう。記事の評価の変更はリダイレクトが必要なほど重要ではないため、理想的にはバックグラウンドでAJAXで行われるべきです。アドオンのハンドリングスクリプト を使用し、AJAXリンクにはCSSクラス ajax を付けるという通常の慣習に従います。

しかし、具体的にはどのようにすればよいでしょうか?Netteは2つの方法を提供します:いわゆる動的スニペットの方法とコンポーネントの方法です。どちらにも長所と短所があるため、それぞれを順番に見ていきます。

動的スニペットの方法

動的スニペットとは、Latteの用語では、スニペット名に変数を使用する {snippet} タグの特定のユースケースを意味します。このようなスニペットはテンプレートのどこにでも配置できるわけではありません – 静的スニペット、つまり通常の、または {snippetArea} 内で囲まれている必要があります。私たちのテンプレートを次のように変更できます。

{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>いいね</a>
			{else}
				<a n:href="unlike! $article->id" class=ajax>いいねを取り消す</a>
			{/if}
		{/snippet}
	</article>
{/snippet}

各記事は、記事IDを名前に含むスニペットを定義します。これらすべてのスニペットは、articlesContainer という名前の1つのスニペットでまとめてラップされます。このラッピングスニペットを省略すると、Latteは例外で警告します。

残っているのは、Presenterに再描画を追加することです – 静的なラッパーを再描画するだけで十分です。

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

同様に、姉妹メソッド handleUnlike() も変更すれば、AJAXは機能します!

しかし、この解決策には1つの欠点があります。AJAXリクエストがどのように進行するかをさらに調査すると、アプリケーションは表面的には効率的に見える(特定の記事に対して1つのスニペットのみを返す)ものの、実際にはサーバー上で全てのスニペットを描画していることがわかります。目的のスニペットをペイロードに配置し、他のスニペットは破棄しました(したがって、それらもデータベースから不必要に取得しました)。

このプロセスを最適化するには、テンプレートに $articles コレクションを渡す場所(例えば renderDefault() メソッド内)に介入する必要があります。シグナルの処理が render<Something> メソッドの前に行われるという事実を利用します。

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

これで、シグナルの処理中に、すべての記事を含むコレクションの代わりに、描画してペイロードでブラウザに送信したい1つの記事のみを含む配列がテンプレートに渡されます。したがって、{foreach} は一度だけ実行され、余分なスニペットは描画されません。

コンポーネントの方法

全く異なる解決方法は、動的スニペットを回避します。トリックは、ロジック全体を特別なコンポーネントに移すことです – これからは、評価の入力はPresenterではなく、専用の LikeControl が担当します。クラスは次のようになります(それに加えて、renderhandleUnlike などのメソッドも含まれます):

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

コンポーネントのテンプレート:

{snippet}
	{if !$article->liked}
		<a n:href="like!" class=ajax>いいね</a>
	{else}
		<a n:href="unlike!" class=ajax>いいねを取り消す</a>
	{/if}
{/snippet}

もちろん、ビューテンプレートが変更され、Presenterにファクトリを追加する必要があります。データベースから取得する記事の数だけコンポーネントを作成するため、その「増殖」には Multiplier クラスを使用します。

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

ビューテンプレートは必要最小限に縮小されます(そして完全にスニペットなし!):

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

ほぼ完了です:アプリケーションはこれでAJAXで動作します。ここでもアプリケーションを最適化する必要があります。なぜなら、Nette Databaseを使用しているため、シグナルの処理中にデータベースから1つではなく、すべての記事が不必要にロードされるからです。しかし、利点は、実際に私たちのコンポーネントだけがレンダリングされるため、それらの描画が行われないことです。