AJAX & snippety

V éře moderních webových aplikací, kde se často rozkládá funkcionalita mezi serverem a prohlížečem, je AJAX nezbytným spojovacím prvkem. Jaké možnosti nám v této oblasti nabízí Nette Framework?

  • odesílání částí šablony, tzv. snippetů
  • předávání proměnných mezi PHP a JavaScriptem
  • nástroje pro debugování AJAXových požadavků

AJAXový požadavek

AJAXový požadavek se v zásadě neliší od klasického HTTP požadavku. Zavolá se presenter s určitými parametry. A je na presenteru, jakým způsobem bude na požadavek reagovat – může vrátit data ve formátu JSON, odeslat část HTML kódu, XML dokument, atd.

Na straně prohlížeče inicializujeme AJAXový požadavek pomocí funkce fetch():

fetch(url, {
	headers: {'X-Requested-With': 'XMLHttpRequest'},
})
.then(response => response.json())
.then(payload => {
	// zpracování odpovědi
});

Na straně serveru rozpoznáme AJAXový požadavek metodou $httpRequest->isAjax() služby zapouzdřující HTTP požadavek. K detekci používá HTTP hlavičku X-Requested-With, proto je důležité ji odesílat. V rámci presenteru lze použít metodu $this->isAjax().

Chcete-li odeslat data ve formátu JSON, použijte metodu sendJson(). Metoda rovněž ukončí činnost presenteru.

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

Máte-li v plánu odpovědět pomocí speciální šablony určené pro AJAX, můžete to udělat následovně:

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

Snippety

Nejsilnější prostředek, který nabízí Nette pro propojení serveru s klientem, představují snippety. Díky nim můžete z obyčejné aplikace udělat AJAXovou jen s minimálním úsilím a několika řádky kódu. Jak to celé funguje demonstruje příklad Fifteen, jehož kód najdete na GitHubu.

Snippety, nebo-li výstřižky, umožnují aktualizovat jen části stránky, místo toho, aby se celá stránka znovunačítala. Jednak je to rychlejší a efektivnější, ale poskytuje to také komfortnější uživatelský zážitek. Snippety vám mohou připomínat Hotwire pro Ruby on Rails nebo Symfony UX Turbo. Zajímavé je, že Nette představilo snippety již o 14 let dříve.

Jak snippety fungují? Při prvním načtení stránky (ne-AJAXovém požadavku) se načte celá stránka včetně všech snippetů. Když uživatel interaguje se stránkou (např. klikne na tlačítko, odešle formulář, atd.), místo načtení celé stránky se vyvolá AJAXový požadavek. Kód v presenteru provede akci a rozhodne, které snippety je třeba aktualizovat. Nette tyto snippety vykreslí a odešle ve formě pole ve formátu JSON. Obslužný kód v prohlížeči získané snippety vloží zpět do stránky. Přenáší se tedy jen kód změněných snippetů, což šetří šířku pásma a zrychluje načítání oproti přenášení obsahu celé stránky.

Naja

K obsluze snippetů na straně prohlížeče slouží knihovna Naja. Tu nainstalujte jako node.js balíček (pro použití s aplikacemi Webpack, Rollup, Vite, Parcel a dalšími):

npm install naja

…nebo přímo vložte do šablony stránky:

<script src="https://unpkg.com/naja@2/dist/Naja.min.js"></script>

Aby se z obyčejného odkazu (signálu) nebo odeslání formuláře vytvořil AJAXový požadavek, stačí označit příslušný odkaz, formulář nebo tlačítko třídou ajax:

<a n:href="go!" class="ajax">Go</a>

<form n:name="form" class="ajax">
    <input n:name="submit">
</form>

nebo

<form n:name="form">
    <input n:name="submit" class="ajax">
</form>

Překreslení snippetů

Každý objekt třídy Control (včetně samotného Presenteru) eviduje, zda došlo ke změnám vyžadujícím jeho překreslení. K tomu slouží metoda redrawControl():

public function handleLogin(string $user): void
{
	// po přihlášení je potřeba překreslit relevantní část
	$this->redrawControl();
	// ...
}

Nette umožňuje ještě jemnější kontrolu toho, co se má překreslit. Uvedená metoda totiž může jako argument přijímat název snippetu. Lze tedy invalidovat (rozuměj: vynutit překreslení) na úrovni částí šablony. Pokud se invaliduje celá komponenta, tak se překreslí i každý její snippet:

// invaliduje snippet 'header'
$this->redrawControl('header');

Snippety v Latte

Používání snippetů v Latte je nesmírně snadné. Chcete-li definovat část šablony jako snippet, obalte ji jednoduše značkami {snippet} a {/snippet}:

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

Snippet vytvoří v HTML stránce element <div> se speciálním vygenerovaným id. Při překreslení snippetu se pak aktulizuje obsah tohoto elementu. Proto je nutné, aby při prvotním vykreslení stránky se vykreslily také všechny snippety, byť mohou být třeba na začátku prázdné.

Můžete vytvořit i snippet s jiným elementem než <div> pomocí n:attributu:

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

Oblasti snippetů

Názvy snippetů mohou být také výrazy:

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

Takto nám vznikne několik snippetů item-0, item-1 atd. Pokud bychom přímo invalidovali dynamický snippet (například item-1), nepřekreslilo by se nic. Důvod je ten, že snippety opravdu fungují jako výstřižky a vykreslují se jen přímo ony samotné. Jenže v šabloně fakticky žádný snippet pojmenovaný item-1 není. Ten vznikne až vykonáváním kódu v okolí snippetu, tedy cyklu foreach. Označíme proto část šablony, která se má vykonat pomocí značky {snippetArea}:

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

A necháme překreslit jak samotný snippet, tak i celou nadřazenou oblast:

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

Zároveň je vhodné zajistit, aby pole $items obsahovalo jen ty položky, které se mají překreslit.

Pokud do šablony vkládáme pomocí značky {include} jinou šablonu, která obsahuje snippety, je nutné vložení šablony opět zahrnout do snippetArea a tu invalidovat společně se snippetem:

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

Snippety v komponentách

Snippety můžete vytvářet i v komponentách a Nette je bude automaticky překreslovat. Ale platí tu určité omezení: pro překreslení snippetů volá metodu render() bez parametrů. Tedy nebude fungovat předávání parametrů v šabloně:

OK
{control productGrid}

nebude fungovat:
{control productGrid $arg, $arg}
{control productGrid:paginator}

Posílání uživatelských dat

Společně se snippety můžete klientovi poslat libovolná další data. Stačí je zapsat do objektu payload:

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

Předávání parametrů

Pokud komponentě pomocí AJAXového požadavku odesíláme parametry, ať už parametry signálu nebo persistentní parametry, musíme u požadavku uvést jejich globální název, který obsahuje i jméno komponenty. Celý název parametru vrací metoda getParameterId().

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

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

A handle metoda s odpovídajícími parametry v komponentě:

public function handleFoo(int $bar): void
{
}
verze: 4.0 3.x 2.x