AJAX y snippets
En la era de las aplicaciones web modernas, donde la funcionalidad suele repartirse entre el servidor y el navegador, AJAX es un elemento de unión imprescindible. ¿Qué posibilidades ofrece Nette Framework en este terreno?
- enviar partes de la plantilla, los llamados snippets
- pasar variables entre PHP y JavaScript
- herramientas para depurar las peticiones AJAX
Petición AJAX
Una petición AJAX no difiere en lo esencial de una petición HTTP clásica. Se llama a un presenter con unos parámetros concretos. Es cosa del presenter decidir cómo responder a la petición: puede devolver datos en formato JSON, enviar una parte del código HTML, un documento XML, etc.
Del lado del navegador iniciamos una petición AJAX con la función fetch():
fetch(url, {
headers: {'X-Requested-With': 'XMLHttpRequest'},
})
.then(response => response.json())
.then(payload => {
// procesa la respuesta
});
Del lado del servidor, una petición AJAX se reconoce con el método $httpRequest->isAjax() del servicio que encapsula la petición HTTP. Usa para detectarla la cabecera HTTP
X-Requested-With, así que es imprescindible enviarla. Dentro del presenter puede usar el método
$this->isAjax().
Si quiere enviar datos en formato JSON, use el método sendJson(). El método termina
además la actividad del presenter.
public function actionExport(): void
{
$this->sendJson($this->model->getData());
}
Si piensa responder con una plantilla especial pensada para AJAX, puede hacerlo así:
public function handleClick($param): void
{
if ($this->isAjax()) {
$this->template->setFile('path/to/ajax.latte');
}
// ...
}
Snippets
La herramienta más potente que ofrece Nette para conectar el servidor con el cliente son los snippets. Con ellos puede convertir una aplicación corriente en una aplicación AJAX con un esfuerzo mínimo y unas pocas líneas de código. El ejemplo Fifteen muestra cómo funciona todo, y su código está en GitHub.
Los snippets permiten actualizar solo partes de la página en lugar de recargarla entera. Esto no solo es más rápido y eficiente, sino que ofrece una experiencia de usuario más cómoda. Los snippets pueden recordarle a Hotwire para Ruby on Rails o a Symfony UX Turbo. Curiosamente, Nette introdujo los snippets 14 años antes.
¿Cómo funcionan los snippets? En la primera carga de la página (una petición no AJAX) se carga la página entera, con todos
los snippets incluidos. Cuando el usuario interactúa con la página (pulsa un botón, envía un formulario, etc.), se inicia una
petición AJAX en lugar de recargar la página entera. El código del presenter ejecuta la acción y decide qué snippets hay que
actualizar. Nette renderiza esos snippets y los envía en una carga útil JSON con un array de snippets. El código de gestión
del navegador inserta después los snippets recibidos de vuelta en la página. Así solo se transfiere el código de los snippets
modificados, lo que ahorra ancho de banda y acelera la carga frente a transferir el contenido de la página entera. Si no se
invalida ningún snippet con redrawControl(), Nette devuelve la página entera incluso en una petición AJAX: los
snippets se envían solo cuando algo se invalida.
Naja
Para gestionar los snippets del lado del navegador se usa la biblioteca Naja. Instálela como paquete de Node.js (para usarla con empaquetadores como Webpack, Rollup, Vite, Parcel y otros):
npm install naja
…o insértela directamente en la plantilla de la página:
<script src="https://unpkg.com/naja@3/dist/Naja.min.js"></script>
Primero hay que inicializar la biblioteca:
naja.initialize();
Para convertir un enlace corriente (una señal) o el envío de un formulario en una petición AJAX, basta con marcar el
enlace, el formulario o el botón correspondiente con la clase 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>
Redibujar los snippets
Todo objeto de la clase Control (incluido el propio Presenter)
lleva la cuenta de si se han producido cambios que exijan redibujarlo. Para eso sirve el método redrawControl():
public function handleLogin(string $user): void
{
// tras el acceso hay que redibujar la parte correspondiente
$this->redrawControl();
// ...
}
Nette permite un control aún más fino de lo que hay que redibujar. El método puede recibir como argumento el nombre del snippet. Así es posible invalidar (es decir, forzar el redibujado) a nivel de partes de la plantilla. Si se invalida el componente entero, se redibujarán también todos los snippets que contiene:
// invalida el snippet 'header'
$this->redrawControl('header');
También puede cancelar una invalidación pendiente con el segundo parámetro $redraw: llamar a
$this->redrawControl('header', redraw: false) marca el snippet como no necesitado de redibujado. La firma completa
es redrawControl(?string $snippet = null, bool $redraw = true).
Snippets en Latte
Usar snippets en Latte es facilísimo. Para definir una parte de la plantilla como snippet, basta con envolverla con las
etiquetas {snippet} y {/snippet}:
{snippet header}
<h1>Hello ... </h1>
{/snippet}
El snippet crea en la página HTML un elemento <div> con un id especial generado. Al redibujar
el snippet se actualiza el contenido de ese elemento. Por eso es necesario que, al renderizar la página por primera vez, se
rendericen también todos los snippets, aunque al principio puedan estar vacíos.
También puede crear un snippet con un elemento distinto de <div> mediante un n:atributo:
<article n:snippet="header" class="foo bar">
<h1>Hello ... </h1>
</article>
Áreas de snippets
Los nombres de los snippets pueden ser también expresiones:
{foreach $items as $id => $item}
<li n:snippet="item-{$id}">{$item}</li>
{/foreach}
Por sí solo, esto es un paso intermedio no funcional: renderizado fuera de un {snippet} o
{snippetArea} estático, un snippet dinámico provoca un E_USER_WARNING con el mensaje Dynamic
snippets are allowed only inside static snippet/snippetArea. Lo arreglamos a continuación.
Esto crea varios snippets como item-0, item-1, etc. Si invalidáramos directamente un snippet
dinámico (por ejemplo, item-1), no se redibujaría nada. El motivo es que los snippets funcionan realmente como
extractos y solo ellos mismos se renderizan directamente. En la plantilla, sin embargo, no existe técnicamente ningún snippet
llamado item-1. Solo nace cuando se ejecuta el código que rodea al snippet, es decir, el bucle foreach. Por eso
marcamos con la etiqueta {snippetArea} la parte de la plantilla que hay que ejecutar:
<ul n:snippetArea="itemsContainer">
{foreach $items as $id => $item}
<li n:snippet="item-{$id}">{$item}</li>
{/foreach}
</ul>
Y pedimos el redibujado tanto del snippet concreto como de toda el área padre:
$this->redrawControl('itemsContainer');
$this->redrawControl('item-1');
Al mismo tiempo, conviene asegurarse de que el array $items contenga solo los elementos que deben redibujarse.
Si incluimos en la plantilla principal otra plantilla que contiene snippets mediante la etiqueta {include}, hay
que envolver de nuevo la inclusión de la plantilla en un snippetArea e invalidarlo junto con el snippet:
{snippetArea include}
{include 'included.latte'}
{/snippetArea}
{* included.latte *}
{snippet item}
...
{/snippet}
$this->redrawControl('include');
$this->redrawControl('item');
Snippets en los componentes
Puede crear snippets dentro de los componentes y Nette los
redibujará automáticamente. Hay, sin embargo, una limitación: para redibujar los snippets, Nette llama al método
render() sin parámetro alguno. Por eso, pasar parámetros en la plantilla no funcionará:
OK
{control productGrid}
will not work:
{control productGrid $arg, $arg}
{control productGrid:paginator}
Enviar datos propios
Junto con los snippets puede enviar al cliente cualquier dato adicional. Basta con escribirlo en el objeto
payload:
public function actionDelete(int $id): void
{
// ...
if ($this->isAjax()) {
$this->payload->message = 'Success';
}
}
Redirección
Durante una petición AJAX, los métodos redirect() y redirectUrl() no envían una redirección HTTP.
En su lugar escriben la URL de destino en la carga útil (el objeto de datos enviado en la respuesta AJAX), en concreto en su
propiedad payload.redirect, y la envían; la redirección propiamente dicha la realiza después la biblioteca del
lado del cliente (Naja).
Paso de parámetros
Al enviar parámetros a un componente mediante una petición AJAX, ya sean parámetros de señal o parámetros persistentes,
debemos indicar en la petición su nombre global, que incluye el nombre del componente. El método getParameterId()
devuelve el nombre completo del parámetro.
let url = new URL({link //foo!});
url.searchParams.set({$control->getParameterId('bar')}, bar);
fetch(url, {
headers: {'X-Requested-With': 'XMLHttpRequest'},
})
Y el método handle con los parámetros correspondientes en el componente:
public function handleFoo(int $bar): void
{
}