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>

Първо е необходимо библиотеката да бъде инициализирана:

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>Hello ... </h1>
{/snippet}

Снипетът създава в HTML страницата елемент <div> със специално генериран 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}

няма да работи:
{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