AJAX y fragmentos

Hoy en día, las aplicaciones web modernas se ejecutan mitad en un servidor y mitad en un navegador. AJAX es un factor de unión vital. ¿Qué soporte ofrece Nette Framework?

  • envío de fragmentos de plantillas (los llamados snippets)
  • paso de variables entre PHP y JavaScript
  • depuración de aplicaciones AJAX

Solicitud AJAX

Una petición AJAX no difiere de una petición clásica: se llama al presentador con una vista y unos parámetros específicos. También depende del presentador cómo responder a ella: puede utilizar su propia rutina, que devuelve un fragmento de código HTML (HTML snippet), un documento XML, un objeto JSON o código JavaScript.

En el lado del servidor, una petición AJAX puede detectarse utilizando el método de servicio que encapsula la petición HTTP $httpRequest->isAjax() (detecta basándose en la cabecera HTTP X-Requested-With). Dentro del presentador, se dispone de un acceso directo en forma del método $this->isAjax().

Existe un objeto preprocesado llamado payload dedicado a enviar datos al navegador en JSON.

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

Para un control total sobre su salida JSON utilice el método sendJson en su presentador. Terminará el presentador inmediatamente y prescindirá de una plantilla:

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

Si queremos enviar HTML, podemos establecer una plantilla especial para peticiones AJAX:

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

Naja

K obsluze AJAXových požadavků na straně prohlížeče slouží knihovna Naja, instálalo como un paquete node.js (para usarlo con Webpack, Rollup, Vite, Parcel y más):

npm install naja

…o insertarlo directamente en la plantilla de la página:

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

Para crear una solicitud AJAX a partir de un enlace normal (señal) o el envío de un formulario, basta con marcar el enlace, formulario o 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>

Fragmentos

Existe una herramienta mucho más potente de soporte AJAX integrado: los snippets. Su uso permite convertir una aplicación normal en una aplicación AJAX utilizando sólo unas pocas líneas de código. Cómo funciona todo se demuestra en el ejemplo Fifteen cuyo código también está accesible en la compilación o en GitHub.

La forma en que funcionan los snippets es que toda la página se transfiere durante la petición inicial (es decir, no AJAX) y luego con cada subpetición AJAX (petición de la misma vista del mismo presentador) sólo se transfiere el código de las partes modificadas en el repositorio payload mencionado anteriormente.

Puede que los snippets te recuerden a Hotwire para Ruby on Rails o a Symfony UX Turbo, pero Nette los inventó catorce años antes.

Invalidación de Snippets

Cada descendiente de la clase Control (que también es un Presentador) es capaz de recordar si hubo algún cambio durante una petición que requiera que se vuelva a renderizar. Hay un par de métodos para manejar esto: redrawControl() y isControlInvalid(). Un ejemplo:

public function handleLogin(string $user): void
{
	// The object has to re-render after the user has logged in
	$this->redrawControl();
	// ...
}

Nette, sin embargo, ofrece una resolución aún más fina que la de los componentes completos. Los métodos mencionados aceptan el nombre de un “fragmento” como parámetro opcional. Un “fragmento” es básicamente un elemento de su plantilla marcado para ese propósito por una tag Latte, más sobre esto más adelante. Así, es posible pedir a un componente que redibuje sólo partes de su plantilla. Si se invalida todo el componente, entonces se vuelven a renderizar todos sus fragmentos. Un componente es “inválido” también si cualquiera de sus subcomponentes es inválido.

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

$this->redrawControl('header'); // invalidates the snippet named 'header'
$this->isControlInvalid('header'); // -> true
$this->isControlInvalid('footer'); // -> false
$this->isControlInvalid(); // -> true, at least one snippet is invalid

$this->redrawControl(); // invalidates the whole component, every snippet
$this->isControlInvalid('footer'); // -> true

Un componente que recibe una señal se marca automáticamente para ser redibujado.

Gracias al redibujado de fragmentos, sabemos exactamente qué partes de qué elementos deben redibujarse.

Etiqueta {snippet} … {/snippet}

El renderizado de la página procede de forma muy similar a una petición normal: se cargan las mismas plantillas, etc. Sin embargo, lo esencial es dejar fuera las partes que no deben llegar a la salida; las demás partes se asociarán a un identificador y se enviarán al usuario en un formato comprensible para un manipulador JavaScript.

Sintaxis

Si hay un control o un fragmento en la plantilla, tenemos que envolverlo usando la etiqueta {snippet} ... {/snippet} pair – se asegurará de que el fragmento renderizado será “recortado” y enviado al navegador. También lo encerrará en una etiqueta helper <div> (es posible utilizar otra). En el siguiente ejemplo se define un fragmento llamado header. También puede representar la plantilla de un componente:

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

Si desea crear un fragmento con un elemento contenedor distinto de <div> o añadir atributos personalizados al elemento, puede utilizar la siguiente definición:

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

Fragmentos dinámicos

En Nette también puede definir fragmentos con un nombre dinámico basado en un parámetro de tiempo de ejecución. Esto es más adecuado para varias listas en las que necesitamos cambiar sólo una fila pero no queremos transferir toda la lista junto con ella. Un ejemplo de esto sería:

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

Hay un fragmento estático llamado itemsContainer, que contiene varios fragmentos dinámicos: item-0 item-1 y así sucesivamente.

No puedes redibujar un fragmento dinámico directamente (redibujar item-1 no tiene ningún efecto), tienes que redibujar su fragmento padre (en este ejemplo itemsContainer). Esto hace que se ejecute el código del fragmento padre, pero entonces sólo se envían al navegador sus sub fragmentos. Si desea enviar sólo uno de los sub fragmentos, debe modificar la entrada del fragmento padre para que no genere los otros sub fragmentos.

En el ejemplo anterior tiene que asegurarse de que para una petición AJAX sólo se añadirá un elemento a la matriz $list, por lo que el bucle foreach sólo imprimirá un fragmento dinámico.

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

Fragmentos en una plantilla incluida

Puede ocurrir que el snippet esté en una plantilla que está siendo incluida desde una plantilla diferente. En ese caso necesitamos envolver el código de inclusión en la segunda plantilla con la tag snippetArea, entonces redibujamos tanto el snippetArea como el snippet real.

La tag snippetArea garantiza que se ejecute el código que contiene, pero que sólo se envíe al navegador el fragmento real de la plantilla incluida.

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

También se puede combinar con fragmentos dinámicos.

Añadir y eliminar

Si añades un nuevo elemento a la lista e invalidas itemsContainer, la petición AJAX devuelve fragmentos que incluyen el nuevo, pero el manejador javascript no podrá renderizarlo. Esto se debe a que no hay ningún elemento HTML con el ID recién creado.

En este caso, la forma más sencilla es envolver toda la lista en un fragmento más e invalidarlo todo:

{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 mismo ocurre con la eliminación de un elemento. Sería posible enviar un fragmento vacío, pero normalmente las listas pueden paginarse y sería complicado implementar la eliminación de un elemento y la carga de otro (que solía estar en una página diferente de la lista paginada).

Envío de parámetros al componente

Cuando enviamos parámetros al componente a través de una petición AJAX, ya sean parámetros de señal o parámetros persistentes, debemos proporcionar su nombre global, que también contiene el nombre del componente. El nombre completo del parámetro devuelve el método getParameterId().

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

Y manejar el método con s parámetros correspondientes en el componente.

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

}
versión: 4.0