AJAX & сніпети
В епоху сучасних веб-застосунків, де функціональність часто розподілена між сервером і браузером, AJAX є необхідним сполучним елементом. Які можливості пропонує нам Nette Framework у цій галузі?
- надсилання частин шаблону, так званих сніпетів
- передача змінних між PHP і JavaScript
- інструменти для налагодження AJAX-запитів
AJAX-запит
AJAX-запит, по суті, не відрізняється від класичного HTTP-запиту. Викликається presenter із певними параметрами. І від 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
, тому важливо його надсилати. У presenter'і можна
використовувати метод $this->isAjax()
.
Якщо ви хочете надіслати дані у форматі JSON, використовуйте метод sendJson()
. Метод також
завершує роботу presenter'а.
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-запит. Код у presenter'і виконує дію і вирішує, які сніпети потрібно оновити. 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">Перейти</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
{
}