AJAX & Snippets

As aplicações web modernas atualmente rodam metade em um servidor e metade em um navegador. O AJAX é um fator de união vital. Que suporte o Nette Framework oferece?

  • envio de fragmentos de modelos (os chamados snippets)
  • passando variáveis entre PHP e JavaScript
  • Depuração de aplicações AJAX

Uma solicitação AJAX pode ser detectada usando um método de um serviço que encapsula uma solicitação HTTP $httpRequest->isAjax() (detecta com base no cabeçalho HTTP X-Requested-With ). Há também um método abreviado no apresentador: $this->isAjax().

Um pedido AJAX não é diferente de um pedido normal – um apresentador é chamado com uma certa visão e parâmetros. Também depende do apresentador como ele irá reagir: ele pode usar suas rotinas para retornar um fragmento de código HTML (um snippet), um documento XML, um objeto JSON ou um pedaço de código Javascript.

Há um objeto pré-processado chamado payload dedicado ao envio de dados para o navegador no JSON.

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

Para um controle total sobre sua saída JSON, utilize o método sendJson em seu apresentador. Ele encerra o apresentador imediatamente e você não precisará de um modelo:

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

Se quisermos enviar HTML, podemos definir um modelo especial para pedidos AJAX:

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

Naja

biblioteca Naja é utilizada para lidar com pedidos AJAX no lado do navegador. Instale-a como um pacote node.js (para usar com Webpack, Rollup, Vite, Parcel e mais):

npm install naja

…ou inseri-lo diretamente no modelo da página:

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

Snippets

Há uma ferramenta muito mais poderosa de suporte AJAX incorporado – trechos. O uso deles torna possível transformar uma aplicação regular em AJAX, utilizando apenas algumas linhas de código. Como tudo funciona é demonstrado no exemplo dos Quinze, cujo código também é acessível no build ou no GitHub.

A forma como os trechos funcionam é que a página inteira é transferida durante a solicitação inicial (isto é, não-AJAX) e depois com cada sub solicitação AJAX (solicitação da mesma visão do mesmo apresentador) apenas o código das partes alteradas é transferido no repositório payload mencionado anteriormente.

Snippets podem lembrá-lo da Hotwire para Ruby on Rails ou Symfony UX Turbo, mas a Nette surgiu com eles catorze anos antes.

Invalidação de Snippets

Cada descendente do Controle de Classe (que um Apresentador também é) é capaz de lembrar se houve alguma mudança durante um pedido que requeira sua reapresentação. Há um par de métodos para lidar com isso: redrawControl() e isControlInvalid(). Um exemplo:

public function handleLogin(string $user): void
{
	// O objeto tem de ser restituído após o usuário ter feito o login
	$this->redrawControl();
	// ...
}

A Nette, entretanto, oferece uma resolução ainda mais fina do que os componentes inteiros. Os métodos listados aceitam o nome do chamado “snippet” como um parâmetro opcional. Um “snippet” é basicamente um elemento em seu modelo marcado para esse fim por uma tag Latte, mais sobre isso depois. Assim, é possível pedir a um componente para redesenhar apenas partes de seu gabarito. Se o componente inteiro for invalidado, então todos os seus trechos serão restituídos. Um componente é “inválido” também se qualquer um de seus subcomponentes for inválido.

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

$this->redrawControl('header'); // invalida o snippet chamado 'header'.
$this->isControlInvalid('header'); // -> true
$this->isControlInvalid('footer'); // -> false
$this->isControlInvalid(); // -> true, at least one snippet is invalid

$this->redrawControl(); // invalida todo o componente, todos os snippet
$this->isControlInvalid('footer'); // -> true

Um componente que recebe um sinal é automaticamente marcado para ser redesenhado.

Graças ao desenho de snippet, sabemos exatamente quais partes de quais elementos devem ser novamente entregues.

Tag {snippet} … {/snippet}

A renderização da página procede de forma muito semelhante a um pedido regular: os mesmos modelos são carregados, etc. A parte vital é, no entanto, deixar de fora as partes que não devem chegar à saída; as outras partes devem ser associadas a um identificador e enviadas ao usuário em um formato compreensível para um manipulador de JavaScript.

Sintaxe

Se houver um controle ou um snippet no modelo, temos que embrulhá-lo usando a tag do par {snippet} ... {/snippet} – ele assegurará que o snippet renderizado será “cortado” e enviado para o navegador. Ele também o anexará em um helper <div> (é possível utilizar uma etiqueta diferente). No exemplo a seguir, um trecho chamado header está definido. Ele pode também representar o modelo de um componente:

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

Um fragmento de um tipo diferente de <div> ou um snippet com atributos HTML adicionais é obtido usando a variante de atributo:

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

Snippets dinâmicos

Em Nette você também pode definir trechos com um nome dinâmico baseado em um parâmetro de tempo de execução. Isto é mais adequado para várias listas onde precisamos mudar apenas uma linha, mas não queremos transferir a lista inteira junto com ela. Um exemplo disto seria:

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

Há um trecho estático chamado itemsContainer, contendo vários trechos dinâmicos: item-0, item-1 e assim por diante.

Você não pode redesenhar um trecho dinâmico diretamente (o redesenho de item-1 não tem efeito), você tem que redesenhar seu trecho pai (neste exemplo itemsContainer). Isto faz com que o código do snippet pai seja executado, mas então apenas seus sub-snippets são enviados para o navegador. Se você quiser enviar apenas um dos sub-snippets, você tem que modificar a entrada para que o trecho pai não gere os outros sub-snippets.

No exemplo acima você tem que ter certeza de que para um pedido AJAX apenas um item será adicionado à matriz $list, portanto o laço foreach imprimirá apenas um trecho 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');
	}
}

Snippets em um Modelo Incluído

Pode acontecer que o trecho esteja em um modelo que está sendo incluído a partir de um modelo diferente. Nesse caso, precisamos embrulhar o código de inclusão no segundo modelo com a tag snippetArea, então redesenhamos tanto o snippetArea quanto o snippet real.

A tag snippetArea assegura que o código interno seja executado, mas apenas o trecho real no modelo incluído é enviado para o navegador.

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

Você também pode combiná-lo com trechos dinâmicos.

Adicionando e excluindo

Se você acrescentar um novo item à lista e invalidar itemsContainer, o pedido AJAX devolve trechos incluindo o novo, mas o manipulador de javascript não será capaz de renderizá-lo. Isto porque não há nenhum elemento HTML com o ID recém-criado.

Neste caso, a maneira mais simples é envolver toda a lista em mais um trecho e invalidar tudo isso:

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

O mesmo vale para a eliminação de um item. Seria possível enviar um trecho vazio, mas geralmente as listas podem ser paginadas e seria complicado implementar a exclusão de um item e o carregamento de outro (que costumava estar em uma página diferente da lista paginada).

Parâmetros de envio para o componente

Quando enviamos parâmetros para o componente via solicitação AJAX, sejam parâmetros de sinal ou parâmetros persistentes, devemos fornecer seu nome global, que também contém o nome do componente. O nome completo do parâmetro retorna o método getParameterId().

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

E método de manuseio com s parâmetros correspondentes em componente.

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

}
versão: 4.0