AJAX & Snippets

În prezent, aplicațiile web moderne rulează pe jumătate pe un server și pe jumătate în browser. AJAX este un factor vital de unificare. Ce suport oferă Nette Framework?

  • trimiterea de fragmente de șabloane (așa-numitele snippets)
  • transmiterea de variabile între PHP și JavaScript
  • depanarea aplicațiilor AJAX

O cerere AJAX poate fi detectată prin intermediul unei metode a unui serviciu care încapsulează o cerere HTTP $httpRequest->isAjax() (detectează pe baza antetului HTTP X-Requested-With ). Există, de asemenea, o metodă prescurtată în presenter: $this->isAjax().

O solicitare AJAX nu diferă cu nimic de una normală – un prezentator este apelat cu o anumită vizualizare și parametri. Depinde, de asemenea, de prezentator cum va reacționa: acesta își poate folosi rutinele pentru a returna fie un fragment de cod HTML (un snippet), fie un document XML, un obiect JSON sau o bucată de cod Javascript.

Există un obiect preprocesat numit payload dedicat trimiterii de date către browser în JSON.

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

Pentru un control complet asupra ieșirii JSON, utilizați metoda sendJson în prezentator. Aceasta termină imediat presenterul și vă veți descurca fără șablon:

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

Dacă dorim să trimitem HTML, putem fie să setăm un șablon special pentru cererile AJAX:

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

Naja

Librăria Naja este utilizată pentru a gestiona cererile AJAX din partea browserului. Instalați-o ca un pachet node.js (pentru a o utiliza cu Webpack, Rollup, Vite, Parcel și altele):

npm install naja

…sau inserați-o direct în șablonul de pagină:

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

Snippets

Există un instrument mult mai puternic de suport AJAX încorporat – snippets. Utilizarea acestora face posibilă transformarea unei aplicații obișnuite într-una AJAX folosind doar câteva linii de cod. Modul în care funcționează totul este demonstrat în exemplul Fifteen, al cărui cod este, de asemenea, accesibil în build sau pe GitHub.

Modul în care funcționează snippet-urile este că întreaga pagină este transferată în timpul cererii inițiale (adică non-AJAX) și apoi, la fiecare subcerere AJAX (cerere a aceleiași vizualizări a aceluiași prezentator), doar codul părților modificate este transferat în depozitul payload menționat anterior.

Snippets vă poate aminti de Hotwire pentru Ruby on Rails sau de Symfony UX Turbo, dar Nette a venit cu ele cu paisprezece ani mai devreme.

Invalidarea Snippets

Fiecare descendent al clasei Control (care este, de asemenea, un prezentator) este capabil să își amintească dacă au existat modificări în timpul unei solicitări care să necesite o nouă redare. Există o pereche de metode pentru a gestiona acest lucru: redrawControl() și isControlInvalid(). Un exemplu:

public function handleLogin(string $user): void
{
	// Obiectul trebuie să se redea după ce utilizatorul s-a logat
	$this->redrawControl();
	// ...
}

Nette oferă însă o rezoluție și mai fină decât componentele întregi. Metodele enumerate acceptă ca parametru opțional numele unui așa-numit “snippet”. Un “snippet” este practic un element din șablonul dvs. marcat în acest scop de o etichetă Latte, mai multe detalii în continuare. Astfel, este posibil să cereți unei componente să redeseneze doar părți din șablonul său. Dacă întreaga componentă este invalidată, atunci toate “snippet-urile” sale sunt reredimensionate. O componentă este “invalidată” și dacă oricare dintre subcomponentele sale este invalidată.

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

$this->redrawControl('header'); // invalidează fragmentul numit "header
$this->isControlInvalid('header'); // -> true
$this->isControlInvalid('footer'); // -> false
$this->isControlInvalid(); // -> true, cel puțin un fragment este invalidat

$this->redrawControl(); // invalidează întreaga componentă, fiecare fragment
$this->isControlInvalid('footer'); // -> true

O componentă care primește un semnal este marcată automat pentru redesenare.

Mulțumită redimensionării fragmentelor, știm exact ce părți din ce elemente trebuie redimensionate.

Etichetă {snippet} … {/snippet}

Redarea paginii se desfășoară în mod similar cu o cerere obișnuită: sunt încărcate aceleași șabloane etc. Partea vitală este, totuși, să se lase deoparte părțile care nu trebuie să ajungă la ieșire; celelalte părți trebuie asociate cu un identificator și trimise utilizatorului într-un format inteligibil pentru un manipulator JavaScript.

Sintaxa

Dacă există un control sau un fragment în șablon, trebuie să îl înfășurăm cu ajutorul etichetei {snippet} ... {/snippet} – aceasta se va asigura că fragmentul redat va fi “tăiat” și trimis către browser. De asemenea, acesta va fi inclus într-un helper <div> (este posibil să se folosească o altă etichetă). În exemplul următor este definit un fragment numit header. Acesta poate reprezenta la fel de bine șablonul unei componente:

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

Un fragment de alt tip decât <div> sau un fragment cu atribute HTML suplimentare se obține prin utilizarea variantei de atribut:

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

Snippets dinamice

În Nette puteți defini, de asemenea, fragmente cu un nume dinamic bazat pe un parametru de execuție. Acest lucru este cel mai potrivit pentru diverse liste în cazul în care trebuie să modificăm doar un singur rând, dar nu dorim să transferăm întreaga listă odată cu el. Un exemplu în acest sens ar fi:

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

Există un fragment static numit itemsContainer, care conține mai multe fragmente dinamice: item-0, item-1 și așa mai departe.

Nu puteți redesena direct un fragment dinamic (redesenarea lui item-1 nu are niciun efect), ci trebuie să redesenați fragmentul său părinte (în acest exemplu itemsContainer). Acest lucru determină executarea codului snippet-ului părinte, dar apoi doar sub-snippet-urile sale sunt trimise către browser. Dacă doriți să trimiteți doar unul dintre sub-snippet-uri, trebuie să modificați intrarea pentru snippet-ul părinte pentru a nu genera celelalte sub-snippet-uri.

În exemplul de mai sus, trebuie să vă asigurați că, pentru o cerere AJAX, doar un singur element va fi adăugat la matricea $list, prin urmare, bucla foreach va imprima doar un singur fragment dinamic.

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');
	}
}

Fragmente într-un șablon inclus

Se poate întâmpla ca fragmentul să se afle într-un șablon care este inclus dintr-un alt șablon. În acest caz, trebuie să înfășurăm codul de includere în cel de-al doilea șablon cu eticheta snippetArea, apoi redesenăm atât snippetArea, cât și fragmentul propriu-zis.

Eticheta snippetArea asigură că codul din interior este executat, dar numai fragmentul real din șablonul inclus este trimis către browser.

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

De asemenea, îl puteți combina cu snippet-uri dinamice.

Adăugarea și ștergerea

Dacă adăugați un nou element în listă și invalidați itemsContainer, cererea AJAX returnează fragmente, inclusiv pe cel nou, dar gestionarul javascript nu va putea să îl redea. Acest lucru se datorează faptului că nu există un element HTML cu ID-ul nou creat.

În acest caz, cea mai simplă metodă este de a îngloba întreaga listă într-un alt fragment și de a-l invalida pe tot:

{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');
}

Același lucru este valabil și pentru ștergerea unui element. Ar fi posibil să se trimită un fragment gol, dar, de obicei, listele pot fi paginate și ar fi complicat să se implementeze ștergerea unui element și încărcarea altuia (care se afla pe o pagină diferită a listei paginate).

Trimiterea parametrilor către componentă

Atunci când trimitem parametrii către componentă prin intermediul unei cereri AJAX, fie că este vorba de parametri de semnal sau de parametri persistenți, trebuie să furnizăm numele global al acestora, care conține și numele componentei. Numele complet al parametrului returnează metoda getParameterId().

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

Și gestionează metoda cu s parametrii corespunzători în componentă.

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

}
versiune: 4.0