Динамічні сніпети
Досить часто під час розробки додатків виникає потреба виконувати 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');
}
Шаблон:
<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-посилання мають 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-запит, то виявимо, що хоча зовні додаток виглядає економним (повертає лише один єдиний сніпет для даної статті), насправді на сервері він відрендерив усі сніпети. Потрібний сніпет він помістив у payload, а решту відкинув (отже, також абсолютно марно отримав їх із бази даних).
Щоб оптимізувати цей процес, нам доведеться втрутитися там, де ми
передаємо колекцію $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');
	}
}
Тепер при обробці сигналу до шаблону передається замість колекції з
усіма статтями лише масив з єдиною статтею – тією, яку ми хочемо
відрендерити та надіслати в payload до браузера. {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}
Звичайно, шаблон view зміниться, і нам доведеться додати до презентера фабрику. Оскільки ми створимо компонент стільки разів, скільки статей отримаємо з бази даних, ми використаємо для його “розмноження” клас 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]);
	});
}
Шаблон view зменшиться до необхідного мінімуму (і повністю позбавиться сніпетів!):
<article n:foreach="$articles as $article">
	<h2>{$article->title}</h2>
	<div class="content">{$article->content}</div>
	{control "likeControl-$article->id"}
</article>
Майже готово: додаток тепер працюватиме за допомогою AJAX. Тут також нас чекає оптимізація додатку, оскільки через використання Nette Database при обробці сигналу марно завантажуються всі статті з бази даних замість однієї. Перевагою, однак, є те, що їх рендеринг не відбудеться, оскільки відрендериться дійсно лише наш компонент.