AJAX & Snippetek

A modern webalkalmazások korában, ahol a funkcionalitás gyakran megoszlik a szerver és a böngésző között, az AJAX elengedhetetlen összekötő elem. Milyen lehetőségeket kínál nekünk a Nette Framework ezen a területen?

  • sablonrészek, úgynevezett snippetek küldése
  • változók átadása PHP és JavaScript között
  • eszközök AJAX kérések debuggolásához

AJAX kérés

Az AJAX kérés alapvetően nem különbözik a klasszikus HTTP kéréstől. Meghív egy presentert bizonyos paraméterekkel. És a presenteren múlik, hogyan reagál a kérésre – visszaadhat adatokat JSON formátumban, küldhet HTML kód egy részét, XML dokumentumot stb.

A böngésző oldalán az AJAX kérést a fetch() függvénnyel inicializáljuk:

fetch(url, {
	headers: {'X-Requested-With': 'XMLHttpRequest'},
})
.then(response => response.json())
.then(payload => {
	// válasz feldolgozása
});

Szerveroldalon az AJAX kérést a HTTP kérést becsomagoló szolgáltatás $httpRequest->isAjax() metódusával ismerjük fel. Az észleléshez a X-Requested-With HTTP fejlécet használja, ezért fontos elküldeni. A presenterben a $this->isAjax() metódus használható.

Ha adatokat szeretne küldeni JSON formátumban, használja a sendJson() metódust. A metódus szintén befejezi a presenter működését.

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

Ha egy speciális, AJAX-hoz szánt sablonnal tervez válaszolni, a következőképpen teheti meg:

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

Snippetek

A Nette által kínált legerősebb eszköz a szerver és a kliens összekapcsolására a snippetek. Ezeknek köszönhetően egy átlagos alkalmazást minimális erőfeszítéssel és néhány sor kóddal AJAX-alapúvá alakíthat. Hogy mindez hogyan működik, azt a Fifteen példa demonstrálja, amelynek kódját a GitHubon találja meg.

A snippetek, vagyis kódrészletek, lehetővé teszik az oldal csak bizonyos részeinek frissítését, ahelyett, hogy az egész oldalt újra kellene tölteni. Ez nemcsak gyorsabb és hatékonyabb, hanem kényelmesebb felhasználói élményt is nyújt. A snippetek emlékeztethetnek a Hotwire for Ruby on Rails vagy a Symfony UX Turbo megoldásokra. Érdekesség, hogy a Nette már 14 évvel korábban bemutatta a snippeteket.

Hogyan működnek a snippetek? Az oldal első betöltésekor (nem AJAX kérés esetén) az egész oldal betöltődik, beleértve az összes snippetet is. Amikor a felhasználó interakcióba lép az oldallal (pl. gombra kattint, űrlapot küld stb.), az egész oldal betöltése helyett egy AJAX kérés indul. A presenterben lévő kód végrehajtja a műveletet, és eldönti, mely snippeteket kell frissíteni. A Nette ezeket a snippeteket rendereli és JSON formátumú tömbként küldi el. A böngészőben lévő kezelő kód a kapott snippeteket visszailleszti az oldalba. Így csak a megváltozott snippetek kódja kerül átvitelre, ami sávszélességet takarít meg és gyorsítja a betöltést az egész oldal tartalmának átvitelével szemben.

Naja

A snippetek böngészőoldali kezelésére a Naja könyvtár szolgál. Ezt telepítse node.js csomagként (Webpack, Rollup, Vite, Parcel és más alkalmazásokkal való használathoz):

npm install naja

…vagy közvetlenül illessze be az oldal sablonjába:

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

Először is inicializálni kell a könyvtárat:

naja.initialize();

Ahhoz, hogy egy egyszerű linkből (signal) vagy űrlapküldésből AJAX kérés legyen, elegendő a megfelelő linket, űrlapot vagy gombot ajax osztállyal megjelölni:

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

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

vagy

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

Snippetek újrarajzolása

Minden Control osztályú objektum (beleértve magát a Presentert is) nyilvántartja, hogy történt-e olyan változás, amely az újrarajzolását igényli. Erre szolgál a redrawControl() metódus:

public function handleLogin(string $user): void
{
	// bejelentkezés után újra kell rajzolni a releváns részt
	$this->redrawControl();
	// ...
}

A Nette még finomabb vezérlést tesz lehetővé afölött, hogy mit kell újrarajzolni. Az említett metódus ugyanis argumentumként fogadhatja a snippet nevét. Így lehet invalidálni (értsd: újrarajzolást kényszeríteni) a sablon részei szintjén. Ha az egész komponenst invalidáljuk, akkor annak minden snippetje is újrarajzolódik:

// invalidálja a 'header' snippetet
$this->redrawControl('header');

Snippetek a Latte-ban

A snippetek használata a Latte-ban rendkívül egyszerű. Ha egy sablonrészt snippetként szeretne definiálni, egyszerűen csomagolja be {snippet} és {/snippet} tag-ekkel:

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

A snippet létrehoz egy <div> elemet a HTML oldalon egy speciális, generált id-val. A snippet újrarajzolásakor ennek az elemnek a tartalma frissül. Ezért szükséges, hogy az oldal első renderelésekor az összes snippet is renderelődjön, még akkor is, ha esetleg kezdetben üresek.

Létrehozhat snippetet <div>-től eltérő elemmel is egy n:attribútum segítségével:

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

Snippet területek

A snippetek nevei kifejezések is lehetnek:

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

Így több snippet jön létre: item-0, item-1 stb. Ha közvetlenül invalidálnánk egy dinamikus snippetet (például item-1), semmi sem rajzolódna újra. Ennek oka az, hogy a snippetek valóban kódrészletekként működnek, és csak közvetlenül önmaguk renderelődnek. Azonban a sablonban valójában nincs item-1 nevű snippet. Az csak a snippet körüli kód, azaz a foreach ciklus végrehajtásakor jön létre. Ezért megjelöljük a sablon azon részét, amelyet végre kell hajtani a {snippetArea} tag segítségével:

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

És újrarajzoltatjuk mind a snippetet magát, mind a teljes szülő területet:

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

Ugyanakkor célszerű biztosítani, hogy az $items tömb csak azokat az elemeket tartalmazza, amelyeket újra kell rajzolni.

Ha a sablonba a {include} tag segítségével egy másik sablont illesztünk be, amely snippeteket tartalmaz, a sablon beillesztését ismét snippetArea-ba kell foglalni, és azt a snippettel együtt kell invalidálni:

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

Snippetek a komponensekben

Snippeteket komponensekben is létrehozhat, és a Nette automatikusan újrarajzolja őket. De van egy korlátozás: a snippetek újrarajzolásához a render() metódust paraméterek nélkül hívja meg. Tehát a paraméterek átadása a sablonban nem fog működni:

OK
{control productGrid}

nem fog működni:
{control productGrid $arg, $arg}
{control productGrid:paginator}

Felhasználói adatok küldése

A snippetekkel együtt tetszőleges további adatokat is küldhet a kliensnek. Egyszerűen írja be őket a payload objektumba:

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

Paraméterek átadása

Ha egy komponensnek AJAX kéréssel paramétereket küldünk, legyenek azok signal paraméterek vagy perzisztens paraméterek, a kérésnél meg kell adnunk a globális nevüket, amely tartalmazza a komponens nevét is. A paraméter teljes nevét a getParameterId() metódus adja vissza.

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

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

És a handle metódus a megfelelő paraméterekkel a komponensben:

public function handleFoo(int $bar): void
{
}
verzió: 4.0