AJAX и сниппеты

В эпоху современных веб-приложений, когда функциональность часто разрывается между сервером и браузером, AJAX является необходимым связующим элементом. Какие возможности предлагает Nette Framework в этой области?

  • передача частей шаблона, так называемых сниппетов
  • передача переменных между PHP и JavaScript
  • инструменты для отладки AJAX-запросов

Запрос AJAX

AJAX-запрос принципиально не отличается от классического HTTP-запроса. Вызывается презентатор с определенными параметрами. Как ответить на запрос, зависит от ведущего – он может вернуть данные в формате JSON, отправить часть HTML-кода, XML-документ и т.д.

На стороне браузера мы инициируем AJAX-запрос с помощью функции fetch():

fetch(url, {
	headers: {'X-Requested-With': 'XMLHttpRequest'},
})
.then(response => response.json())
.then(payload => {
	// обработка ответа
});

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

Если необходимо отправить данные в формате JSON, используйте метод sendJson() метод. Метод также завершает работу презентера.

public function actionExport(): void
{
	$this->sendJson($this->model->getData);
}

Если вы планируете отвечать с помощью специального шаблона, предназначенного для AJAX, то это можно сделать следующим образом:

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

Фрагменты

Наиболее мощным инструментом, предлагаемым Nette для связи сервера с клиентом, являются сниппеты. С их помощью можно превратить обычное приложение в AJAX-приложение, затратив минимум усилий и написав несколько строк кода. Пример Fifteen демонстрирует, как это работает, а его код можно найти на GitHub.

Сниппеты, или вырезки, позволяют обновлять только отдельные части страницы, а не перезагружать ее целиком. Это быстрее и эффективнее, а также обеспечивает более комфортную работу пользователя. Возможно, сниппеты напомнят вам Hotwire для Ruby on Rails или Symfony UX Turbo. Интересно, что компания Nette представила сниппеты на 14 лет раньше.

Как работают сниппеты? При первой загрузке страницы (не-AJAX-запрос) загружается вся страница, включая все сниппеты. Когда пользователь взаимодействует со страницей (например, нажимает кнопку, отправляет форму и т.д.), вместо загрузки всей страницы выполняется AJAX-запрос. Код в презентере выполняет это действие и решает, какие фрагменты необходимо обновить. Nette рендерит эти фрагменты и отправляет их в виде массива JSON. Затем код обработки в браузере вставляет полученные фрагменты обратно на страницу. Таким образом, передается только код измененных фрагментов, что позволяет экономить пропускную способность и ускорять загрузку по сравнению с передачей всего содержимого страницы.

Naja

Для работы со сниппетами на стороне браузера используется библиотека Naja. Установите ее как пакет 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>

Перерисовка фрагментов

Каждый объект класса Control (в том числе и сам Presenter) хранит информацию о том, произошли ли изменения, требующие его перерисовки. Для этого используется метод redrawControl().

public function handleLogin(string $user): void
{
	// после входа в систему необходимо перерисовать соответствующую часть
	$this->redrawControl();
	//...
}

Nette также позволяет более тонко контролировать, что именно нужно перерисовывать. Упомянутый выше метод может принимать в качестве аргумента имя фрагмента. Таким образом, можно аннулировать (то есть принудительно перерисовать) на уровне части шаблона. Если аннулируется весь компонент, то перерисовывается и каждый его фрагмент:

// аннулирует фрагмент 'header'
$this->redrawControl('header');

Фрагменты в Latte

Использовать сниппеты в Latte очень просто. Чтобы определить часть шаблона как сниппет, достаточно обернуть ее в теги {snippet} и {/snippet}:

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

Сниппет создает элемент <div> в HTML-странице со специально сгенерированным id. При перерисовке сниппета содержимое этого элемента обновляется. Поэтому при первоначальном рендеринге страницы все сниппеты также должны быть рендерированы, даже если они изначально могут быть пустыми.

Также можно создать сниппет с элементом, отличным от <div> с помощью атрибута n:attribute:

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

Области фрагментов

Имена сниппетов также могут быть выражениями:

{foreach $items as $id => $item}
	<li n:snippet="item-{$id}">{$item}</li>
{/foreach}

Таким образом, мы получим несколько фрагментов типа item-0, item-1, и т.д. Если мы напрямую аннулируем динамический сниппет (например, item-1), то ничего не будет перерисовано. Причина в том, что сниппеты работают как настоящие выдержки, и непосредственно отрисовываются только они сами. Однако в шаблоне технически нет фрагмента с именем item-1. Он появляется только при выполнении окружающего сниппета кода, в данном случае цикла foreach. Поэтому часть шаблона, которую необходимо выполнить, мы пометим тегом {snippetArea}:

<ul n:snippetArea="itemsContainer">
	{foreach $items as $id => $item}
		<li n:snippet="item-{$id}">{$item}</li>
	{/foreach}
</ul>

И перерисуем как отдельный фрагмент, так и всю общую область:

$this->redrawControl('itemsContainer');
$this->redrawControl('item-1');

Также необходимо убедиться, что массив $items содержит только те элементы, которые должны быть перерисованы.

При вставке другого шаблона в основной с помощью тега {include}, содержащего сниппеты, необходимо снова обернуть включаемый шаблон в snippetArea и сделать недействительными и сниппет, и область вместе:

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

Сниппеты в компонентах

Вы можете создавать сниппеты внутри компонентов, и Nette будет автоматически перерисовывать их. Однако есть одно ограничение: для перерисовки сниппетов вызывается метод render() без параметров. Таким образом, передача параметров в шаблоне не работает:

OK
{control productGrid}

will not work:
{control productGrid $arg, $arg}
{control productGrid:paginator}

Отправка пользовательских данных

Наряду со сниппетами можно отправлять клиенту любые дополнительные данные. Для этого достаточно записать их в объект payload:

public function actionDelete(int $id): void
{
	//...
	if ($this->isAjax()) {
		$this->payload->message = 'Success';
	}
}

Параметры отправки

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

let url = new URL({link //foo!});
url.searchParams.set({$control->getParameterId('bar')}, bar);

fetch(url, {
	headers: {'X-Requested-With': 'XMLHttpRequest'},
})

handle метод с соответствующими параметрами в компоненте:

public function handleFoo(int $bar): void
{
}
версия: 4.0