AJAX e Snippet

Le moderne applicazioni web oggi vengono eseguite per metà su un server e per metà in un browser. AJAX è un fattore di unione vitale. Quale supporto offre il Nette Framework?

  • invio di frammenti di template (i cosiddetti snippet)
  • passaggio di variabili tra PHP e JavaScript
  • debug delle applicazioni AJAX

Richiesta AJAX

Una richiesta AJAX non differisce da una richiesta classica: il presentatore viene chiamato con una vista e dei parametri specifici. Il presentatore può anche decidere come rispondere: può usare la propria routine, che restituisce un frammento di codice HTML (snippet HTML), un documento XML, un oggetto JSON o codice JavaScript.

Sul lato server, una richiesta AJAX può essere rilevata utilizzando il metodo di servizio che incapsula la richiesta HTTP $httpRequest->isAjax() (rileva in base all'intestazione HTTP X-Requested-With). All'interno del presentatore, è disponibile una scorciatoia sotto forma del metodo $this->isAjax().

Esiste un oggetto pre-elaborato chiamato payload, dedicato all'invio di dati al browser in JSON.

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

Per un controllo completo sull'output JSON, utilizzare il metodo sendJson nel presenter. Il metodo termina immediatamente il presentatore e si può fare a meno di un template:

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

Se vogliamo inviare HTML, possiamo impostare un modello speciale per le richieste AJAX:

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

Naja

La libreria Naja è usata per gestire le richieste AJAX sul lato browser. Installarla come pacchetto node.js (da usare con Webpack, Rollup, Vite, Parcel e altri):

npm install naja

… o inserirla direttamente nel modello della pagina:

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

Per creare una richiesta AJAX da un normale link (segnale) o dall'invio di un modulo, è sufficiente contrassegnare il relativo link, modulo o pulsante con la classe ajax:

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

Snippet

Esiste uno strumento molto più potente del supporto AJAX integrato: gli snippet. Il loro utilizzo consente di trasformare una normale applicazione in un'applicazione AJAX utilizzando solo poche righe di codice. Il funzionamento è dimostrato nell'esempio di Fifteen, il cui codice è accessibile nella build o su GitHub.

Il modo in cui funzionano gli snippet è che l'intera pagina viene trasferita durante la richiesta iniziale (cioè non AJAX) e poi a ogni sotto-richiesta AJAX (richiesta della stessa vista dello stesso presentatore) viene trasferito solo il codice delle parti modificate nel repository payload menzionato in precedenza.

Gli snippet possono ricordare Hotwire per Ruby on Rails o Symfony UX Turbo, ma Nette li ha ideati quattordici anni prima.

Invalidazione degli Snippet

Ogni discendente della classe Control (di cui fa parte anche un Presentatore) è in grado di ricordare se durante una richiesta sono state apportate modifiche che richiedono un nuovo rendering. Esistono due metodi per gestire questo aspetto: redrawControl() e isControlInvalid(). Un esempio:

public function handleLogin(string $user): void
{
	// L'oggetto deve essere ri-renderizzato dopo che l'utente ha effettuato il login
	$this->redrawControl();
	// ...
}

Nette offre tuttavia una risoluzione ancora più fine rispetto ai componenti interi. I metodi elencati accettano il nome di un cosiddetto “snippet” come parametro opzionale. Uno “snippet” è fondamentalmente un elemento del modello contrassegnato a tale scopo da una tag di Latte, di cui si parlerà più avanti. In questo modo è possibile chiedere a un componente di ridisegnare solo parti del suo modello. Se l'intero componente viene invalidato, tutti i suoi frammenti vengono ridisegnati. Un componente è “invalido” anche se uno qualsiasi dei suoi sottocomponenti è invalido.

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

$this->redrawControl('header'); // invalida lo snippet denominato 'header'
$this->isControlInvalid('header'); // -> true
$this->isControlInvalid('footer'); // -> false
$this->isControlInvalid(); // -> true, almeno uno snippet non è valido

$this->redrawControl(); // invalida l'intero componente, ogni snippet
$this->isControlInvalid('footer'); // -> true

Un componente che riceve un segnale viene automaticamente contrassegnato per essere ridisegnato.

Grazie allo snippet redrawing sappiamo esattamente quali parti di quali elementi devono essere ridisegnate.

Tag {snippet} … {/snippet}

Il rendering della pagina procede in modo molto simile a una normale richiesta: vengono caricati gli stessi template, ecc. La parte fondamentale è, tuttavia, lasciare fuori le parti che non devono raggiungere l'output; le altre parti devono essere associate a un identificatore e inviate all'utente in un formato comprensibile per un gestore JavaScript.

Sintassi

Se c'è un controllo o uno snippet nel template, dobbiamo avvolgerlo usando il tag di coppia {snippet} ... {/snippet} – che farà in modo che lo snippet reso venga “tagliato” e inviato al browser. Lo racchiuderà anche in un tag di aiuto <div> (è possibile utilizzarne uno diverso). Nell'esempio seguente viene definito uno snippet chiamato header. Può anche rappresentare il modello di un componente:

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

Uno snippet di tipo diverso da <div> o uno snippet con attributi HTML aggiuntivi si ottiene utilizzando la variante dell'attributo:

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

Snippet dinamici

In Nette è possibile definire snippet con un nome dinamico basato su un parametro di runtime. Ciò è particolarmente indicato per vari elenchi in cui è necessario modificare una sola riga, ma non si vuole trasferire l'intero elenco. Un esempio potrebbe essere:

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

Esiste uno snippet statico chiamato itemsContainer, che contiene diversi snippet dinamici: item-0, item-1 e così via.

Non è possibile ridisegnare direttamente uno snippet dinamico (il ridisegno di item-1 non ha alcun effetto), ma è necessario ridisegnare il suo snippet padre (in questo esempio itemsContainer). Questo provoca l'esecuzione del codice dello snippet padre, ma poi solo i suoi sotto-snippet vengono inviati al browser. Se si vuole inviare solo uno dei sotto-nippet, è necessario modificare l'input dello snippet padre per non generare gli altri sotto-nippet.

Nell'esempio precedente, bisogna assicurarsi che per una richiesta AJAX venga aggiunto un solo elemento all'array $list, quindi il ciclo foreach stamperà un solo frammento dinamico.

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

Snippet in un template incluso

Può accadere che lo snippet si trovi in un template che viene incluso da un template diverso. In questo caso, occorre avvolgere il codice di inclusione nel secondo template con la tag snippetArea, quindi ridisegnare sia la snippetArea che lo snippet vero e proprio.

La tag snippetArea assicura che il codice all'interno venga eseguito, ma che al browser venga inviato solo lo snippet effettivo nel modello incluso.

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

Si può anche combinare con gli snippet dinamici.

Aggiunta e cancellazione

Se si aggiunge un nuovo elemento all'elenco e si invalida itemsContainer, la richiesta AJAX restituisce gli snippet che includono il nuovo elemento, ma il gestore javascript non sarà in grado di renderlo. Questo perché non esiste un elemento HTML con l'ID appena creato.

In questo caso, il modo più semplice è avvolgere l'intero elenco in un altro frammento e invalidare il tutto:

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

Lo stesso vale per la cancellazione di un elemento. Sarebbe possibile inviare uno snippet vuoto, ma di solito gli elenchi possono essere paginati e sarebbe complicato implementare la cancellazione di un elemento e il caricamento di un altro (che si trovava in una pagina diversa dell'elenco paginato).

Invio di parametri al componente

Quando si inviano parametri al componente tramite una richiesta AJAX, sia che si tratti di parametri di segnale che di parametri persistenti, occorre fornire il loro nome globale, che contiene anche il nome del componente. Il nome completo del parametro restituisce il metodo getParameterId().

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

E gestire il metodo con i parametri corrispondenti nel componente.

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

}
versione: 4.0