AJAX и фрагменти

Съвременните уеб приложения днес се изпълняват наполовина на сървъра и наполовина в браузъра. AJAX е важен обединяващ фактор. Каква поддръжка предлага рамката Nette?

  • Подаване на фрагменти (наречени фрагменти)
  • прехвърляне на променливи между PHP и JavaScript
  • отстраняване на грешки в приложенията AJAX

Заявка AJAX

Заявката AJAX не се различава от класическата заявка – водещият се извиква с определен изглед и параметри. От водещия също зависи как да отговори на нея: той може да използва своя собствена процедура, която връща фрагмент от HTML код (HTML snippet), XML документ, JSON обект или JavaScript код.

От страна на сървъра AJAX заявката може да бъде открита с помощта на метода на услугата, капсулиращ HTTP заявката $httpRequest->isAjax() (открива се въз основа на HTTP заглавието X-Requested-With). Вътре в презентатора е наличен пряк път под формата на метода $this->isAjax().

Съществува предварително обработен обект payload, предназначен за изпращане на данни към браузъра във формат JSON.

public function actionDelete(int $id): void
{
	if ($this->isAjax()) {
		$this->payload->message = 'Успешно';
	}
	// ...
}

За да имате пълен контрол върху извеждането на JSON, използвайте метода sendJson в презентатора. Това веднага ще прекъсне презентатора и ще се справите без шаблона:

$this->sendJson(['key' => 'value', /* ... */]);

Ако искаме да изпращаме HTML, можем да създадем специален шаблон за AJAX заявки:

public function handleClick($param): void
{
	if ($this->isAjax()) {
		$this->template->setFile('path/to/ajax.latte');
	}
	// ...
}

Naja

Библиотеката Naja се използва за обработка на AJAX заявки от страна на браузъра. Инсталирайте го като пакет за node.js (за използване с Webpack, Rollup, Vite, Parcel и други):

npm install naja

…или да вмъкнете директно в шаблон на страница:

<script src="https://unpkg.com/naja@2/dist/Naja.min.js"></script>

За да създадете AJAX заявка от обикновена връзка (сигнал) или подаване на формуляр, просто маркирайте съответната връзка, формуляр или бутон с класа ajax:

<a n:href="go!" class="ajax">Go</a>

<form n:name="form" class="ajax">
    <input n:name="submit">
</form>

or
<form n:name="form">
    <input n:name="submit" class="ajax">
</form>

Извадки

Има обаче много по-мощен инструмент за вградена поддръжка на AJAX – фрагменти. Използването им ви позволява да превърнете обикновено приложение в AJAX приложение само с няколко реда код. Как работи всичко това, е показано в примера Fifteen, чийто код също е наличен в компилацията или в GitHub.

Принципът на фрагментите е, че цялата страница се прехвърля по време на първоначалната (т.е. не-AJAX) заявка и след това при всяка AJAX подзаявка (заявка на същия изглед от същия водещ) само кодът на променените части се прехвърля в хранилището payload, споменато по-рано.

Извадките може да ви напомнят за Hotwire за Ruby on Rails или Symfony UX Turbo, но Nette ги изобретява четиринадесет години по-рано.

Инвалидизация

Всеки наследник на класа Control (какъвто е Presenter) може да запомни дали по време на заявката е имало промени, които изискват повторно картографиране. Съществуват няколко начина за справяне с това: redrawControl() и isControlInvalid(). Пример:

public function handleLogin(string $user): void
{
	// Обектът трябва да бъде показан отново, след като потребителят е влязъл в системата
	$this->redrawControl();
	// ...
}

Nette обаче осигурява още по-фина разделителна способност от целите компоненти. Изброените методи приемат името на така наречения “фрагмент” като незадължителен параметър. “Фрагмент” по същество е елемент от вашия шаблон, маркиран с макрос Latte за тази цел, за който ще стане дума по-късно. По този начин можете да поискате от компонент да прерисува само част от шаблона. Ако целият компонент е невалиден, всички негови фрагменти се прерисуват. Компонент е “невалиден”, ако някой от неговите подкомпоненти е невалиден.

$this->isControlInvalid(); // -> false

$this->redrawControl('header'); // обезсилване на фрагмента с име 'header'
$this->isControlInvalid('header'); // -> true
$this->isControlInvalid('footer'); // -> false
$this->isControlInvalid(); // -> true, поне един фрагмент е невалиден

$this->redrawControl(); // обезсилва целия компонент, всеки фрагмент
$this->isControlInvalid('footer'); // -> true

Компонент, който е получил сигнал, автоматично се маркира за прерисуване.

Чрез преначертаване на срезовете знаем точно кои части от кои елементи трябва да бъдат преначертани.

Етикет {snippet} … {/snippet}

Страницата се визуализира по същия начин, както при обикновена заявка: зареждат се същите шаблони и т.н. Най-важното обаче е да се предотвратят онези части, които не трябва да се извеждат; останалите части трябва да се свържат с идентификатор и да се изпратят на потребителя във формат, който обработващият JavaScript може да разбере.

Синтаксис

Ако в шаблона има контролен елемент или фрагмент, трябва да го обгърнем с помощта на сдвоения таг {snippet} ... {/snippet} – визуализираният фрагмент ще бъде “изрязан” и изпратен на браузъра. Освен това той ще го обгърне със спомагателен таг <div> (можете да използвате друга). Следващият пример дефинира фрагмент с име header. Той може да представлява и шаблон на компонент:

{snippet header}
	<h1>Hello ... </h1>
{/snippet}

Ако искате да създадете фрагмент със съдържащ елемент, различен от <div>, или да добавите потребителски атрибути към елемента, можете да използвате следното определение:

<article n:snippet="header" class="foo bar">
	<h1>Hello ... </h1>
</article>

Динамични фрагменти

В Nette можете също така да дефинирате фрагменти с динамично име въз основа на параметър по време на изпълнение. Това е най-подходящо за различни списъци, в които трябва да променим само един ред, но не искаме да преместим целия списък заедно с него. Пример за това е:

<ul n:snippet="itemsContainer">
	{foreach $list as $id => $item}
		<li n:snippet="item-$id">{$item} <a class="ajax" n:href="update! $id">обновить</a></li>
	{/foreach}
</ul>

Съществува един статичен фрагмент itemsContainer, който съдържа няколко динамични фрагмента: пункт-0, пункт-1 и т.н.

Не можете да прерисувате динамичния фрагмент директно (прерисуването на item-1 няма ефект), а трябва да прерисувате родителския фрагмент ( itemsContainer в този пример). При това се изпълнява кодът на родителския фрагмент, но на браузъра се предават само неговите вложени фрагменти. Ако искате да предадете само един от вложените фрагменти, трябва да промените входа за родителския фрагмент, за да избегнете генерирането на други вложени фрагменти.

В горния пример трябва да се уверите, че само един елемент ще бъде добавен към масива $list, когато бъде направена заявката AJAX, така че цикълът foreach ще изведе само един динамичен фрагмент.

class HomePresenter extends Nette\Application\UI\Presenter
{
	/**
	 * Этот метод возвращает данные для списка.
	 * Обычно это просто запрос данных из модели.
	 * Для целей этого примера данные жёстко закодированы.
	 */
	private function getTheWholeList(): array
	{
		return [
			'First',
			'Second',
			'Third',
		];
	}

	public function renderDefault(): void
	{
		if (!isset($this->template->list)) {
			$this->template->list = $this->getTheWholeList();
		}
	}

	public function handleUpdate(int $id): void
	{
		$this->template->list = $this->isAjax()
				? []
				: $this->getTheWholeList();
		$this->template->list[$id] = 'Updated item';
		$this->redrawControl('itemsContainer');
	}
}

Извадки в активирания шаблон

Възможно е даден фрагмент да се намира в шаблон, който е включен от друг шаблон. В този случай е необходимо да се обгърне кодът за разрешаване във втория шаблон с макроса snippetArea, след което да се прерисуват както областта snippetArea, така и самият фрагмент.

Макросът snippetArea гарантира, че кодът в него ще бъде изпълнен, но само действителният фрагмент от включения шаблон ще бъде изпратен на браузъра.

{* parent.latte *}
{snippetArea wrapper}
	{include 'child.latte'}
{/snippetArea}
{* child.latte *}
{snippet item}
...
{/snippet}
$this->redrawControl('wrapper');
$this->redrawControl('item');

Можете също така да го комбинирате с динамични фрагменти.

Добавяне и премахване

Ако добавите нов елемент в списъка и отмените itemsContainer, заявката AJAX ще върне фрагменти, включващи новия елемент, но обработващият javascript няма да може да го визуализира. Това е така, защото няма HTML елемент с новосъздадения ID.

В този случай най-лесният начин е да обвиете целия списък в друг фрагмент и да го обезсилите:

{snippet wholeList}
<ul n:snippet="itemsContainer">
	{foreach $list as $id => $item}
	<li n:snippet="item-$id">{$item} <a class="ajax" n:href="update! $id">обновить</a></li>
	{/foreach}
</ul>
{/snippet}
<a class="ajax" n:href="add!">Добавить</a>
public function handleAdd(): void
{
	$this->template->list = $this->getTheWholeList();
	$this->template->list[] = 'New one';
	$this->redrawControl('wholeList');
}

Същото важи и за изтриване на елемент. Може да се подаде празен фрагмент, но обикновено списъците могат да бъдат странирани и би било трудно да се приложи премахването на един елемент и зареждането на друг (който преди това е бил на друга страница на странирания списък).

Изпращане на параметри към компонент

Когато изпращаме параметри към компонент чрез заявка AJAX, независимо дали става въпрос за сигнални или постоянни параметри, трябва да предоставим тяхното глобално име, което съдържа и името на компонента. Пълното име на параметъра се връща от метода getParameterId().

$.getJSON(
	{link changeCountBasket!},
	{
		{$control->getParameterId('id')}: id,
		{$control->getParameterId('count')}: count
	}
});

И обработете метода със съответните параметри в компонента.

public function handleChangeCountBasket(int $id, int $count): void
{

}
версия: 4.0