Dynamic Snippets
Quite often in application development there is a need to perform AJAX operations, for example, in individual rows of a table or list items. As an example, we can choose to list articles, allowing the logged-in user to select a “like/dislike” rating for each of them. The code of the presenter and the corresponding template without AJAX will look something like this (I list the most important snippets, the code assumes the existence of a service for marking up the ratings and getting a collection of articles – the specific implementation is not important for the purposes of this tutorial):
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
Let's now bring AJAX to this simple application. Changing the rating of an article is not important enough to require a HTTP
request with redirect, so ideally it should be done with AJAX in the background. We'll use the handler script from add-ons with the usual convention that AJAX
links have the CSS class ajax
.
However, how to do it specifically? Nette offers 2 ways: the dynamic snippet way and the component way. Both of them have their pros and cons, so we will show them one by one.
The Dynamic Snippets Way
In Latte terminology, a dynamic snippet is a specific use case of the {snippet}
tag where a variable is used in
the snippet name. Such a snippet cannot be found just anywhere in the template – it must be wrapped by a static snippet, i.e. a
regular one, or 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 single snippet, which has an article ID in the title. All these snippets are then wrapped together
in a single snippet called articlesContainer
. If we omit this wrapping snippet, Latte will alert us with an
exception.
All that's left to do is to add redrawing to the presenter – just 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); -- není potřeba
} else {
$this->redirect('this');
}
}
Modify the sister method handleUnlike()
in the same way, and AJAX is up and running!
The solution has one downside, however. If we dig more into how the AJAX request works, we find that although the application looks efficient in appearance (it only returns a single snippet for a given article), it actually renders all the snippets on the server. It has placed the desired snippet in our payload, and discarded the others (thus, quite unnecessarily, it also retrieved them from the database).
To optimize this process, we'll need to take action where we pass the $articles
collection to the template (say in
the renderDefault()
method). We will take advantage of the fact that signal processing takes place 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, when the signal is processed, instead of a collection with all articles, only an array with a single article is passed to
the template – the one we want to render and send in payload to the browser. Thus, {foreach}
will be done only
once and no extra snippets will be rendered.
Component Way
A completely different solution uses a different approach to avoid dynamic snippets. The trick is to move all the logic into a
separate component – from now on, we don't have a presenter to take care of entering the rating, but a dedicated
LikeControl
. The class will look like the following (in addition, it will also contain the 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}
Of course we will change the view template and we will have to add a factory to the presenter. Since we will create the component as many times as we receive articles from the database, we will use the Multiplier class to “multiply” it.
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 template view is reduced to the minimum necessary (and 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 are almost done: the application will now work in AJAX. Here too we have to optimize the application, because due to the use of Nette Database, the signal processing will unnecessarily load all articles from the database instead of one. However, the advantage is that there will be no rendering, because only our component is actually rendered.