Динамические сниппеты
Довольно часто при разработке приложений возникает необходимость выполнения операций AJAX, например, в отдельных строках таблицы или элементах списка. В качестве примера, мы можем выбрать список статей, позволяя вошедшему в систему пользователю выбрать «нравится/не нравится» для каждой из них. Код презентера и соответствующего шаблона без AJAX будет выглядеть примерно так (я перечисляю наиболее важные фрагменты, код предполагает наличие сервиса для разметки рейтингов и получения коллекции статей — конкретная реализация не важна для целей данного руководства):
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>Мне нравится</a>
{else}
<a n:href="unlike! $article->id" class=ajax>Мне не нравится</a>
{/if}
</article>
Аяксизация
Теперь давайте привнесем AJAX в это простое приложение. Изменение
рейтинга статьи не настолько важно, чтобы требовать HTTP-запрос с
перенаправлением, поэтому в идеале это должно быть сделано с помощью
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
. Если мы опустим этот фрагмент обертки, Latte
предупредит нас об исключении.
Всё, что осталось сделать, это добавить перерисовку в презентер — просто перерисовать статическую обертку.
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
будет работать!
Однако у этого решения есть и обратная сторона. Если мы подробнее рассмотрим, как работает AJAX-запрос, то обнаружим, что хотя приложение выглядит эффективным внешне (оно возвращает только один сниппет для данной статьи), на самом деле оно отображает все сниппеты на сервере. Он поместил нужный фрагмент в нашу полезную нагрузку, а остальные отбросил (таким образом, совершенно без необходимости, он также извлек их из базы данных).
Чтобы оптимизировать этот процесс, нам понадобится действие, при
котором мы передаем коллекцию $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');
}
}
Теперь, когда сигнал обрабатывается, вместо коллекции со всеми
статьями в шаблон передается только массив с одной статьей — той,
которую мы хотим отобразить и отправить в полезной нагрузке браузеру.
Таким образом, {foreach}
будет выполнен только один раз, и никаких
дополнительных сниппетов не будет выведено.
Компонентный способ
Совершенно другое решение использует другой подход, чтобы избежать
динамических сниппетов. Хитрость заключается в том, чтобы перенести
всю логику в отдельный компонент — отныне у нас не презентер, который
будет заботиться о вводе рейтинга, а специальный 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}
Конечно, мы изменим шаблон представления, и нам придется добавить фабрику к презентеру. Поскольку мы будем создавать компонент столько раз, сколько статей мы получим из базы данных, мы будем использовать класс 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, обработка сигнала будет излишне загружать все статьи из базы данных вместо одной. Однако преимущество в том, что рендеринга не будет, потому что на самом деле рендерится только наш компонент.