AJAX e snippet
Nell'era delle applicazioni web moderne, in cui le funzionalità sono spesso ripartite tra il server e il browser, AJAX è l'elemento di collegamento indispensabile. Quali possibilità offre Nette Framework in questo campo?
- l'invio di parti del template, i cosiddetti snippet
- il passaggio di variabili tra PHP e JavaScript
- strumenti per il debugging delle richieste AJAX
Richiesta AJAX
Una richiesta AJAX non differisce sostanzialmente da una classica richiesta HTTP. Viene chiamato un presenter con determinati parametri. Sta al presenter decidere come rispondere alla richiesta: può restituire dati in formato JSON, inviare una parte di codice HTML, un documento XML e così via.
Sul lato browser avviamo una richiesta AJAX con la funzione fetch():
fetch(url, {
headers: {'X-Requested-With': 'XMLHttpRequest'},
})
.then(response => response.json())
.then(payload => {
// elabora la risposta
});
Sul lato server una richiesta AJAX si riconosce con il metodo $httpRequest->isAjax() del servizio che incapsula la richiesta HTTP. Per il rilevamento usa l'header HTTP
X-Requested-With, quindi è essenziale inviarlo. Dentro il presenter potete usare il metodo
$this->isAjax().
Se volete inviare dati in formato JSON, usate il metodo sendJson(). Il metodo termina
anche l'attività del presenter.
public function actionExport(): void
{
$this->sendJson($this->model->getData());
}
Se avete in programma di rispondere con un template speciale pensato per AJAX, potete farlo così:
public function handleClick($param): void
{
if ($this->isAjax()) {
$this->template->setFile('path/to/ajax.latte');
}
// ...
}
Snippet
Lo strumento più potente che Nette offre per collegare il server al client sono gli snippet. Con essi potete trasformare una normale applicazione in una applicazione AJAX con il minimo sforzo e poche righe di codice. L'esempio Fifteen mostra come funziona il tutto e il suo codice si trova su GitHub.
Gli snippet permettono di aggiornare solo parti della pagina, invece di ricaricarla per intero. Non è solo più veloce ed efficiente, ma offre anche un'esperienza d'uso più comoda. Gli snippet vi ricorderanno forse Hotwire per Ruby on Rails o Symfony UX Turbo. È curioso che Nette abbia introdotto gli snippet 14 anni prima.
Come funzionano gli snippet? Al primo caricamento della pagina (una richiesta non AJAX) viene caricata l'intera pagina, snippet
compresi. Quando l'utente interagisce con la pagina (per esempio clicca un pulsante, invia un form ecc.), invece di ricaricare
l'intera pagina viene avviata una richiesta AJAX. Il codice del presenter esegue l'azione e decide quali snippet vanno aggiornati.
Nette disegna questi snippet e li invia come payload JSON contenente un array con gli snippet. Il codice che li gestisce nel
browser inserisce poi gli snippet ricevuti nella pagina. Viene quindi trasferito solo il codice degli snippet cambiati, il che
risparmia banda e accelera il caricamento rispetto al trasferimento dell'intero contenuto della pagina. Se nessuno snippet viene
invalidato con redrawControl(), Nette restituisce l'intera pagina anche per una richiesta AJAX: gli snippet vengono
inviati solo quando qualcosa è stato invalidato.
Naja
Per gestire gli snippet sul lato browser si usa la libreria Naja. Installatela come pacchetto Node.js (per l'uso con bundler come Webpack, Rollup, Vite, Parcel e altri):
npm install naja
…oppure inseritela direttamente nel template della pagina:
<script src="https://unpkg.com/naja@3/dist/Naja.min.js"></script>
Per prima cosa dovete inizializzare la libreria:
naja.initialize();
Per trasformare un normale link (segnale) o l'invio di un form in una richiesta AJAX, basta contrassegnare il link, il form
o il pulsante con la classe ajax:
<a n:href="go!" class="ajax">Vai</a>
<form n:name="form" class="ajax">
<input n:name="submit">
</form>
oppure
<form n:name="form">
<input n:name="submit" class="ajax">
</form>
Ridisegnare gli snippet
Ogni oggetto della classe Control (compreso il presenter stesso)
tiene traccia del fatto che siano avvenuti cambiamenti che ne richiedono il ridisegno. A questo scopo si usa il metodo
redrawControl():
public function handleLogin(string $user): void
{
// dopo il login è necessario ridisegnare la parte interessata
$this->redrawControl();
// ...
}
Nette permette un controllo ancora più fine su ciò che va ridisegnato. Il metodo può ricevere come argomento il nome dello snippet. È quindi possibile invalidare (cioè forzare il ridisegno) a livello di singole parti del template. Se viene invalidato l'intero componente, verranno ridisegnati anche tutti i suoi snippet:
// invalida lo snippet 'header'
$this->redrawControl('header');
Potete anche annullare un'invalidazione in sospeso con il secondo parametro $redraw: chiamando
$this->redrawControl('header', redraw: false) contrassegnate lo snippet come non bisognoso di ridisegno. La firma
completa è redrawControl(?string $snippet = null, bool $redraw = true).
Snippet in Latte
Usare gli snippet in Latte è estremamente semplice. Per definire una parte del template come snippet basta racchiuderla tra
i tag {snippet} e {/snippet}:
{snippet header}
<h1>Ciao ... </h1>
{/snippet}
Lo snippet crea nella pagina HTML un elemento <div> con un id speciale generato. Quando lo
snippet viene ridisegnato, il contenuto di questo elemento viene aggiornato. È quindi necessario che al primo rendering della
pagina vengano disegnati anche tutti gli snippet, anche se all'inizio potrebbero essere vuoti.
Potete creare uno snippet anche con un elemento diverso da <div>, usando un n:attributo:
<article n:snippet="header" class="foo bar">
<h1>Ciao ... </h1>
</article>
Aree snippet
I nomi degli snippet possono essere anche espressioni:
{foreach $items as $id => $item}
<li n:snippet="item-{$id}">{$item}</li>
{/foreach}
Di per sé è un passaggio intermedio non funzionante: disegnato fuori da uno {snippet} o
{snippetArea} statico, uno snippet dinamico emette un E_USER_WARNING con il messaggio Dynamic
snippets are allowed only inside static snippet/snippetArea. Lo sistemiamo qui sotto.
Così vengono creati diversi snippet come item-0, item-1 ecc. Se invalidassimo direttamente uno
snippet dinamico (per esempio item-1), non verrebbe ridisegnato nulla. Il motivo è che gli snippet funzionano
davvero come estratti e vengono disegnati direttamente solo loro stessi. Nel template, però, tecnicamente non esiste alcuno
snippet chiamato item-1: esso nasce solo quando viene eseguito il codice che circonda lo snippet, cioè il ciclo
foreach. Contrassegniamo quindi con il tag {snippetArea} la parte del template che deve essere eseguita:
<ul n:snippetArea="itemsContainer">
{foreach $items as $id => $item}
<li n:snippet="item-{$id}">{$item}</li>
{/foreach}
</ul>
E chiediamo il ridisegno sia del singolo snippet sia dell'intera area genitore:
$this->redrawControl('itemsContainer');
$this->redrawControl('item-1');
Allo stesso tempo è opportuno assicurarsi che l'array $items contenga solo gli elementi da ridisegnare.
Se nel template principale includiamo con il tag {include} un altro template che contiene snippet, è necessario
racchiudere di nuovo l'inclusione del template in uno snippetArea e invalidarlo insieme allo snippet:
{snippetArea include}
{include 'included.latte'}
{/snippetArea}
{* included.latte *}
{snippet item}
...
{/snippet}
$this->redrawControl('include');
$this->redrawControl('item');
Snippet nei componenti
Potete creare snippet dentro i componenti e Nette li
ridisegnerà automaticamente. C'è però un limite: per ridisegnare gli snippet Nette chiama il metodo render() senza
alcun parametro. Passare parametri nel template non funzionerà quindi:
OK
{control productGrid}
non funzionerà:
{control productGrid $arg, $arg}
{control productGrid:paginator}
Inviare dati personalizzati
Insieme agli snippet potete inviare al client qualsiasi dato aggiuntivo. Basta scriverlo nell'oggetto payload:
public function actionDelete(int $id): void
{
// ...
if ($this->isAjax()) {
$this->payload->message = 'Success';
}
}
Redirect
Durante una richiesta AJAX i metodi redirect() e redirectUrl() non inviano un redirect HTTP.
Scrivono invece l'URL di destinazione nel payload (l'oggetto dati inviato nella risposta AJAX), precisamente nella sua proprietà
payload.redirect, e lo inviano; il redirect vero e proprio viene poi eseguito dalla libreria lato client (Naja).
Passare i parametri
Quando inviamo dei parametri a un componente tramite una richiesta AJAX, siano essi parametri di segnale o parametri
persistenti, dobbiamo indicarne nella richiesta il nome globale, che comprende il nome del componente. Il metodo
getParameterId() restituisce il nome completo del parametro.
let url = new URL({link //foo!});
url.searchParams.set({$control->getParameterId('bar')}, bar);
fetch(url, {
headers: {'X-Requested-With': 'XMLHttpRequest'},
})
E il metodo handle con i parametri corrispondenti nel componente:
public function handleFoo(int $bar): void
{
}