AJAX и сниппеты
В эпоху современных веб-приложений, где функциональность часто распределяется между сервером и браузером, AJAX является необходимым связующим элементом. Какие возможности предлагает нам Nette Framework в этой области?
- отправка частей шаблона, так называемых сниппетов
- передача переменных между PHP и JavaScript
- инструменты для отладки AJAX-запросов
AJAX-запрос
AJAX-запрос в принципе не отличается от классического HTTP-запроса. Вызывается презентер (presenter) с определенными параметрами. И презентер решает, как реагировать на запрос — он может вернуть данные в формате 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-запрос. Код в презентере выполняет действие (action) и решает, какие сниппеты необходимо обновить. 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>
Сначала необходимо инициализировать библиотеку:
naja.initialize();
Чтобы превратить обычную ссылку (сигнал) или отправку формы в
AJAX-запрос, достаточно пометить соответствующую ссылку, форму или
кнопку классом ajax
:
<a n:href="go!" class="ajax">Go</a>
<form n:name="form" class="ajax">
<input n:name="submit">
</form>
или
<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>Привет ... </h1>
{/snippet}
Сниппет создает в HTML-странице элемент <div>
со специальным
сгенерированным id
. При перерисовке сниппета обновляется
содержимое этого элемента. Поэтому необходимо, чтобы при
первоначальной отрисовке страницы отрисовывались также все сниппеты,
даже если они могут быть пустыми вначале.
Вы можете создать сниппет с другим элементом, отличным от
<div>
, с помощью n:атрибута:
<article n:snippet="header" class="foo bar">
<h1>Привет ... </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}
не будет работать:
{control productGrid $arg, $arg}
{control productGrid:paginator}
Отправка пользовательских данных
Вместе со сниппетами вы можете отправить клиенту любые другие
данные. Достаточно записать их в объект payload
:
public function actionDelete(int $id): void
{
// ...
if ($this->isAjax()) {
$this->payload->message = 'Успешно';
}
}
Передача параметров
Если мы отправляем компоненту параметры с помощью 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
{
}