AJAX & snippety

În era aplicațiilor web moderne, unde funcționalitatea este adesea împărțită între server și browser, AJAX este un element de legătură esențial. Ce opțiuni ne oferă Nette Framework în acest domeniu?

  • trimiterea unor părți din șablon, așa-numitele snippets
  • transmiterea variabilelor între PHP și JavaScript
  • instrumente pentru depanarea cererilor AJAX

Cererea AJAX

O cerere AJAX nu diferă, în esență, de o cerere HTTP clasică. Se apelează un presenter cu anumiți parametri. Și depinde de presenter cum va reacționa la cerere – poate returna date în format JSON, poate trimite o parte din codul HTML, un document XML etc.

Pe partea de browser, inițializăm cererea AJAX folosind funcția fetch():

fetch(url, {
	headers: {'X-Requested-With': 'XMLHttpRequest'},
})
.then(response => response.json())
.then(payload => {
	// procesarea răspunsului
});

Pe partea de server, recunoaștem o cerere AJAX prin metoda $httpRequest->isAjax() a serviciului încapsulând cererea HTTP. Pentru detectare, utilizează antetul HTTP X-Requested-With, de aceea este important să îl trimitem. În cadrul presenterului, se poate utiliza metoda $this->isAjax().

Dacă doriți să trimiteți date în format JSON, utilizați metoda sendJson(). Metoda încheie, de asemenea, activitatea presenterului.

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

Dacă intenționați să răspundeți folosind un șablon special destinat AJAX, puteți face acest lucru după cum urmează:

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

Snippets

Cel mai puternic instrument pe care Nette îl oferă pentru conectarea serverului cu clientul sunt snippets. Datorită lor, puteți transforma o aplicație obișnuită într-una AJAX cu un efort minim și câteva linii de cod. Exemplul Fifteen, al cărui cod îl găsiți pe GitHub, demonstrează cum funcționează totul.

Snippets, sau fragmente, permit actualizarea doar a unor părți ale paginii, în loc de a reîncărca întreaga pagină. Acest lucru este nu numai mai rapid și mai eficient, dar oferă și o experiență de utilizare mai confortabilă. Snippets vă pot aminti de Hotwire pentru Ruby on Rails sau Symfony UX Turbo. Interesant este că Nette a introdus snippets cu 14 ani mai devreme.

Cum funcționează snippets? La prima încărcare a paginii (cerere non-AJAX), se încarcă întreaga pagină, inclusiv toate snippets. Când utilizatorul interacționează cu pagina (de exemplu, face clic pe un buton, trimite un formular etc.), în loc de a încărca întreaga pagină, se declanșează o cerere AJAX. Codul din presenter execută acțiunea și decide ce snippets trebuie actualizate. Nette redă aceste snippets și le trimite sub forma unui array în format JSON. Codul de gestionare din browser inserează snippets primite înapoi în pagină. Astfel, se transferă doar codul snippets modificate, ceea ce economisește lățimea de bandă și accelerează încărcarea în comparație cu transferul conținutului întregii pagini.

Naja

Pentru gestionarea snippets pe partea de browser, se utilizează biblioteca Naja. Aceasta se instalează ca pachet node.js (pentru utilizare cu aplicații Webpack, Rollup, Vite, Parcel și altele):

npm install naja

…sau se inserează direct în șablonul paginii:

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

Mai întâi, este necesar să inițializați biblioteca:

naja.initialize();

Pentru a transforma un link obișnuit (semnal) sau trimiterea unui formular într-o cerere AJAX, este suficient să marcați linkul, formularul sau butonul respectiv cu clasa ajax:

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

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

sau

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

Redesenarea snippetelor

Fiecare obiect al clasei Control (inclusiv Presenterul însuși) înregistrează dacă au avut loc modificări care necesită redesenarea sa. Pentru aceasta se utilizează metoda redrawControl():

public function handleLogin(string $user): void
{
	// după logare este necesar să redesenăm partea relevantă
	$this->redrawControl();
	// ...
}

Nette permite un control și mai fin asupra a ceea ce trebuie redesenat. Metoda menționată poate primi ca argument numele snippetului. Astfel, se poate invalida (adică forța redesenarea) la nivelul părților șablonului. Dacă se invalidează întreaga componentă, se va redesena și fiecare snippet al acesteia:

// invalidează snippetul 'header'
$this->redrawControl('header');

Snippets în Latte

Utilizarea snippetelor în Latte este extrem de ușoară. Dacă doriți să definiți o parte a șablonului ca snippet, încadrați-o pur și simplu între tag-urile {snippet} și {/snippet}:

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

Snippetul creează în pagina HTML un element <div> cu un id special generat. La redesenarea snippetului, se actualizează conținutul acestui element. De aceea, este necesar ca la redarea inițială a paginii să se redea și toate snippet-urile, chiar dacă acestea pot fi inițial goale.

Puteți crea și un snippet cu un alt element decât <div> folosind n:atributul:

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

Zone de snippets

Numele snippetelor pot fi și expresii:

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

Astfel, vom crea mai multe snippets item-0, item-1 etc. Dacă am invalida direct un snippet dinamic (de exemplu, item-1), nu s-ar redesena nimic. Motivul este că snippet-urile funcționează într-adevăr ca fragmente și se redau doar ele însele direct. Însă, în șablon, nu există de fapt niciun snippet numit item-1. Acesta apare doar prin executarea codului din jurul snippetului, adică ciclul foreach. Prin urmare, marcăm porțiunea de șablon care trebuie executată folosind tag-ul {snippetArea}:

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

Și lăsăm să se redeseneze atât snippetul însuși, cât și întreaga zonă părinte:

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

În același timp, este recomandabil să ne asigurăm că array-ul $items conține doar acele elemente care trebuie redesenate.

Dacă includem în șablon, folosind tag-ul {include}, un alt șablon care conține snippets, este necesar să includem din nou includerea șablonului în snippetArea și să o invalidăm împreună cu snippetul:

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

Snippets în componente

Puteți crea snippets și în componente, iar Nette le va redesena automat. Dar există o anumită limitare: pentru redesenarea snippetelor, se apelează metoda render() fără parametri. Prin urmare, nu va funcționa transmiterea parametrilor în șablon:

OK
{control productGrid}

nu va funcționa:
{control productGrid $arg, $arg}
{control productGrid:paginator}

Trimiterea datelor utilizatorului

Împreună cu snippets, puteți trimite clientului orice alte date. Este suficient să le scrieți în obiectul payload:

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

Transmiterea parametrilor

Dacă trimitem parametri unei componente printr-o cerere AJAX, fie că sunt parametri de semnal sau parametri persistenți, trebuie să specificăm în cerere numele lor global, care include și numele componentei. Numele complet al parametrului este returnat de metoda getParameterId().

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

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

Și metoda handle cu parametrii corespunzători în componentă:

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