AJAX & Snippetek

A modern webes alkalmazások manapság félig a szerveren, félig a böngészőben futnak. Az AJAX létfontosságú egyesítő tényező. Milyen támogatást nyújt a Nette Framework?

  • sablonrészletek küldése (ún. snippetek)
  • változók átadása PHP és JavaScript között
  • AJAX-alkalmazások hibakeresése

AJAX kérés

Az AJAX-kérés nem különbözik a klasszikus kéréstől – a bemutatót egy adott nézettel és paraméterekkel hívják meg. Az is a prezenteren múlik, hogyan válaszol rá: használhat saját rutint, amely egy HTML kódrészletet (HTML snippet), egy XML-dokumentumot, egy JSON-objektumot vagy JavaScript-kódot ad vissza.

Kiszolgálói oldalon az AJAX-kérés a HTTP-kérést kapszulázó szolgáltatási módszerrel detektálható $httpRequest->isAjax() (a HTTP fejléc alapján detektál X-Requested-With). A prezenteren belül a $this->isAjax() metódus formájában egy rövidítés áll rendelkezésre.

Létezik egy payload nevű előfeldolgozott objektum, amely arra szolgál, hogy az adatokat JSON-ban küldje el a böngészőnek.

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

A JSON kimenet teljes ellenőrzéséhez használja a sendJson metódust a prezenterben. Ez azonnal befejezi a prezentert, és sablon nélkül is boldogulsz:

$this->sendJson(['key' => 'value', /* ... */]);

Ha HTML-t szeretnénk küldeni, akkor vagy beállíthatunk egy speciális sablont az AJAX-kérésekhez:

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

Naja

Naja könyvtár az AJAX kérések kezelésére szolgál a böngésző oldalán. Telepítsd node.js csomagként (Webpack, Rollup, Vite, Parcel és más csomagokkal való használathoz):

npm install naja

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

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

Ahhoz, hogy AJAX-kérést hozzon létre egy hagyományos linkből (jel) vagy űrlapküldésből, egyszerűen jelölje meg az adott linket, űrlapot vagy gombot a ajax osztállyal:

<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>

Snippets

A beépített AJAX-támogatásnak van egy sokkal hatékonyabb eszköze – a snippetek. Használatuk lehetővé teszi, hogy egy hagyományos alkalmazásból AJAX-alkalmazássá váljon mindössze néhány sornyi kóddal. Hogy mindez hogyan működik, azt a Fifteen példa mutatja be, amelynek kódja a buildben vagy a GitHubon is elérhető.

A snippetek úgy működnek, hogy a kezdeti (azaz nem AJAX) kérés során a teljes oldal átkerül, majd minden AJAX alkérésnél (ugyanazon bemutató azonos nézetének kérése) csak a módosított részek kódja kerül át a már említett payload tárolóba.

A Snippetek a Ruby on Rails Hotwire vagy a Symfony UX Turbo-ra emlékeztethetnek, de Nette tizennégy évvel korábban találta ki őket.

A Snippetek érvénytelenítése

Control osztály minden leszármazottja (ami egy Presenter is) képes megjegyezni, hogy egy kérés során történt-e olyan változás, ami miatt újra kell rendeznie. Van egy pár módszer ennek kezelésére: redrawControl() és isControlInvalid(). Egy példa:

public function handleLogin(string $user): void
{
	// Az objektumot újra kell renderelni, miután a felhasználó bejelentkezett.
	$this->redrawControl();
	// ...
}

A Nette azonban még finomabb felbontást kínál, mint az egész komponensek. A felsorolt módszerek opcionális paraméterként elfogadják egy úgynevezett “snippet” nevét. A “snippet” alapvetően a sablonod egy eleme, amelyet erre a célra egy Latte makróval jelöltél meg, erről később. Így lehetőség van arra, hogy egy komponenst arra kérjünk, hogy csak a sablonjának részeit rajzolja újra. Ha a teljes komponens érvénytelenítésre kerül, akkor az összes snippetje újrarendezésre kerül. Egy komponens akkor is “érvénytelen”, ha bármelyik alkomponense érvénytelen.

$this->isControlInvalid(); // -> false

$this->redrawControl('header'); // érvényteleníti a 'header' nevű snippet-t.
$this->isControlInvalid('header'); // -> true
$this->isControlInvalid('footer'); // -> false
$this->isControlInvalid(); // -> true, legalább egy snippet érvénytelen.

$this->redrawControl(); // érvényteleníti az egész komponenst, minden egyes snippet-tel együtt.
$this->isControlInvalid('footer'); // -> true

A jelzést kapó komponens automatikusan újrarajzolásra kerül.

A snippet-újrarajzolásnak köszönhetően pontosan tudjuk, hogy mely elemek mely részeit kell újrarajzolni.

Tag {snippet} … {/snippet}

Az oldal megjelenítése nagyon hasonlóan zajlik, mint egy normál kérésnél: ugyanazok a sablonok töltődnek be stb. A lényeges rész azonban az, hogy a kimenetre nem szánt részek kimaradnak; a többi részhez egy azonosítót kell társítani, és egy JavaScript kezelő számára érthető formátumban kell elküldeni a felhasználónak.

Szintaxis

Ha a sablonban van egy vezérlőelem vagy egy snippet, akkor azt a {snippet} ... {/snippet} páros taggel kell becsomagolnunk – ez biztosítja, hogy a renderelt snippet “kivágásra” kerüljön, és elküldjük a böngészőnek. Ez is egy segítőbe fogja beburkolni. <div> tagbe (lehet másikat is használni). A következő példában egy header nevű snippet van definiálva. Ez akár egy komponens sablonját is jelentheti:

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

A snippet egy más típusú snippet, mint a <div> vagy egy további HTML-attribútumokkal ellátott snippet az attribútumváltozat használatával érhető el:

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

Dynamic Snippets

A Nette-ben egy futásidejű paraméter alapján dinamikus névvel ellátott snippeteket is definiálhat. Ez leginkább olyan különböző listákhoz alkalmas, ahol csak egy sort kell megváltoztatnunk, de nem akarjuk vele együtt az egész listát is átvinni. Egy példa erre a következő lenne:

<ul n:snippet="itemsContainer">
	{foreach $list as $id => $item}
		<li n:snippet="item-$id">{$item} <a class="ajax" n:href="update! $id">update</a></li>
	{/foreach}
</ul>

Van egy statikus snippet, a itemsContainer, amely több dinamikus snippet tartalmaz: item-0, item-1 és így tovább.

Egy dinamikus snippet közvetlenül nem rajzolható újra (a item-1 újrarajzolásának nincs hatása), a szülő snippetjét (ebben a példában itemsContainer) kell újrarajzolni. Ennek hatására a szülő snippet kódja végrehajtódik, de ezután csak az alszippetjei kerülnek elküldésre a böngészőnek. Ha csak az egyik alrészletet szeretné átküldeni, akkor a szülő részlet bemenetét úgy kell módosítania, hogy a többi alrészletet ne generálja.

A fenti példában gondoskodnia kell arról, hogy egy AJAX-kérés esetén csak egy elem kerüljön a $list tömbhöz, ezért a foreach ciklus csak egy dinamikus snippetet fog kiírni.

class HomePresenter extends Nette\Application\UI\Presenter
{
	/**
	 * This method returns data for the list.
	 * Usually this would just request the data from a model.
	 * For the purpose of this example, the data is hard-coded.
	 */
	private function getTheWholeList(): array
	{
		return [
			'First',
			'Second',
			'Third',
		];
	}

	public function renderDefault(): void
	{
		if (!isset($this->template->list)) {
			$this->template->list = $this->getTheWholeList();
		}
	}

	public function handleUpdate(int $id): void
	{
		$this->template->list = $this->isAjax()
				? []
				: $this->getTheWholeList();
		$this->template->list[$id] = 'Updated item';
		$this->redrawControl('itemsContainer');
	}
}

Snippetek egy belefoglalt sablonban

Előfordulhat, hogy a snippet egy olyan sablonban van, amely egy másik sablonból van beépítve. Ebben az esetben a második sablonban a snippetArea makróval be kell csomagolnunk a beillesztési kódot, majd újra kell rajzolnunk mind a snippetArea-t, mind a tényleges snippetet.

A snippetArea makró biztosítja, hogy a benne lévő kód végrehajtásra kerül, de csak a tényleges snippet kerül a böngészőhöz a bevont sablonban.

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

Dinamikus snippetekkel is kombinálható.

Hozzáadás és törlés

Ha új elemet adsz hozzá a listához, és érvényteleníted a itemsContainer címet, az AJAX-kérés az új elemet is tartalmazó részleteket küldi vissza, de a javascript kezelő nem tudja megjeleníteni azt. Ennek oka, hogy nincs olyan HTML-elem, amely az újonnan létrehozott azonosítóval rendelkezik.

Ebben az esetben a legegyszerűbb megoldás, ha az egész listát még egy snippetbe csomagoljuk, és az egészet érvénytelenítjük:

{snippet wholeList}
<ul n:snippet="itemsContainer">
	{foreach $list as $id => $item}
	<li n:snippet="item-$id">{$item} <a class="ajax" n:href="update! $id">update</a></li>
	{/foreach}
</ul>
{/snippet}
<a class="ajax" n:href="add!">Add</a>
public function handleAdd(): void
{
	$this->template->list = $this->getTheWholeList();
	$this->template->list[] = 'New one';
	$this->redrawControl('wholeList');
}

Ugyanez vonatkozik egy elem törlésére is. Lehetséges lenne üres snippet küldése, de általában a listák lehetnek oldalszámozottak, és bonyolult lenne megvalósítani egy elem törlését és egy másik betöltését (amely korábban a oldalszámozott lista egy másik oldalán volt).

Paraméterek küldése a komponensnek

Amikor AJAX-kérésen keresztül paramétereket küldünk a komponensnek, legyenek azok jelparaméterek vagy állandó paraméterek, 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.

$.getJSON(
	{link changeCountBasket!},
	{
		{$control->getParameterId('id')}: id,
		{$control->getParameterId('count')}: count
	}
});

És kezelje a módszert s megfelelő paraméterekkel a komponensben.

public function handleChangeCountBasket(int $id, int $count): void
{

}
verzió: 4.0