動的スニペット
アプリケーション開発において、テーブルの個々の行やリストの項目などに対して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
が担当します。クラスは次のようになります(それに加えて、render
、handleUnlike
などのメソッドも含まれます):
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つではなく、すべての記事が不必要にロードされるからです。しかし、利点は、実際に私たちのコンポーネントだけがレンダリングされるため、それらの描画が行われないことです。