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