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

Lecturas adicionales

versión: 4.x