AJAX та сніппети

В епоху сучасних веб-додатків, де функціональність часто поширюється між сервером і браузером, AJAX є важливим сполучним елементом. Які можливості пропонує Nette Framework у цій сфері?

  • надсилання частин шаблону, так званих фрагментів (snippets)
  • передача змінних між 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. Встановіть її як пакет 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 (в тому числі і сам доповідач) зберігає інформацію про те, чи відбулися зміни, які вимагають його перемальовування. Для цього використовується метод 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::

<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'},
})

Метод обробки з відповідними параметрами в компоненті:

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