Dynamic Snippets
Quite often during application development, the need arises to perform AJAX operations, for example, on individual rows of a table or list items. As an example, let's consider listing articles where logged-in users can rate each article with ‘like’ or ‘dislike’. The presenter code and corresponding template without AJAX would look something like this (showing the most relevant parts; the code assumes a service exists for handling ratings and retrieving articles – the specific implementation isn't crucial for this guide):
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');
}
Template:
<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>I like it</a>
{else}
<a n:href="unlike! $article->id" class=ajax>I don't like it anymore</a>
{/if}
</article>
Ajaxization
Now, let's add AJAX functionality to this simple application. Changing an article's rating isn't critical enough to warrant a
full page redirect, so it should ideally happen via AJAX in the background. We'll use the handler script from add-ons with the common convention that AJAX
links have the CSS class ajax
.
But how exactly do we implement this? Nette offers two approaches: dynamic snippets and components. Both have their pros and cons, so we'll demonstrate each one.
The Dynamic Snippets Way
In Latte terminology, a dynamic snippet refers to a specific use of the {snippet}
tag where a variable is used in
the snippet's name. Such a snippet cannot be placed just anywhere in the template; it must be wrapped by a static (regular)
snippet or be inside a {snippetArea}
. We could modify our template as follows:
{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>I like it</a>
{else}
<a n:href="unlike! $article->id" class=ajax>I don't like it anymore</a>
{/if}
{/snippet}
</article>
{/snippet}
Each article now defines a snippet whose name includes the article's ID. All these dynamic snippets are then wrapped together
by a static snippet named articlesContainer
. If we were to omit this outer snippet, Latte would throw an
exception.
All that remains is to add the redrawing logic to the presenter – simply redraw the static wrapper.
public function handleLike(int $articleId): void
{
$this->ratingService->saveLike($articleId, $this->user->id);
if ($this->isAjax()) {
$this->redrawControl('articlesContainer');
// $this->redrawControl('article-' . $articleId); -- not needed
} else {
$this->redirect('this');
}
}
Modify the corresponding handleUnlike()
method similarly, and AJAX is functional!
However, this solution has a drawback. If we examine the AJAX request more closely, we'll find that while the application appears efficient externally (returning only a single snippet for the specific article), it actually renders all snippets on the server side. It places the required snippet into the payload and discards the others (meaning it also unnecessarily retrieved and rendered them).
To optimize this, we need to intervene where the $articles
collection is passed to the template (let's say in the
renderDefault()
method). We'll leverage the fact that signal handling occurs before the
render<Something>
methods:
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');
}
}
Now, during signal processing, instead of passing the entire collection of articles, only an array containing the single
relevant article is passed to the template – the one we intend to render and send in the payload to the browser. Consequently,
the {foreach}
loop runs only once, and no unnecessary snippets are rendered.
Component Way
A completely different approach avoids dynamic snippets altogether. The trick involves encapsulating the entire logic within a
separate component. Instead of the presenter handling the rating, a dedicated LikeControl
will manage it. The class
will look like this (it would also contain render
, handleUnlike
, etc. methods):
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');
}
}
}
Template of component:
{snippet}
{if !$article->liked}
<a n:href="like!" class=ajax>I like it</a>
{else}
<a n:href="unlike!" class=ajax>I don't like it anymore</a>
{/if}
{/snippet}
Naturally, the view's template will change, and we'll need to add a factory to the presenter. Since we'll create an instance of this component for each article retrieved from the database, we'll use the Multiplier class to manage their creation.
protected function createComponentLikeControl()
{
$articles = $this->db->table('articles');
return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) {
return new LikeControl($articles[$articleId]);
});
}
The view's template shrinks to the bare minimum (and is completely free of snippets!):
<article n:foreach="$articles as $article">
<h2>{$article->title}</h2>
<div class="content">{$article->content}</div>
{control "likeControl-$article->id"}
</article>
We're almost finished: the application will now function with AJAX. Here too, optimization is needed because, due to the use of Nette Database, signal processing unnecessarily loads all articles from the database instead of just the relevant one. The advantage, however, is that no unnecessary rendering occurs, as only the specific component instance is rendered.