AJAX & Snippets

Na era dos aplicativos modernos da Web, em que a funcionalidade geralmente se estende entre o servidor e o navegador, o AJAX é um elemento de conexão essencial. Que opções o Nette Framework oferece nessa área?

  • envio de partes do modelo, os chamados snippets
  • passagem de variáveis entre PHP e JavaScript
  • ferramentas para depuração de solicitações AJAX

Solicitação de AJAX

Basicamente, uma solicitação AJAX não difere de uma solicitação HTTP clássica. Um apresentador é chamado com parâmetros específicos. Cabe ao apresentador decidir como responder à solicitação: ele pode retornar dados no formato JSON, enviar uma parte do código HTML, um documento XML etc.

No lado do navegador, iniciamos uma solicitação AJAX usando a função fetch():

fetch(url, {
	headers: {'X-Requested-With': 'XMLHttpRequest'},
})
.then(response => response.json())
.then(payload => {
	// processamento da resposta
});

No lado do servidor, uma solicitação AJAX é reconhecida pelo método $httpRequest->isAjax() do serviço que encapsula a solicitação HTTP. Ele usa o cabeçalho HTTP X-Requested-With, portanto, é essencial enviá-lo. No apresentador, você pode usar o método $this->isAjax().

Se você quiser enviar dados no formato JSON, use o método sendJson() método. O método também encerra a atividade do apresentador.

public function actionExport(): void
{
	$this->sendJson($this->model->getData);
}

Se você planeja responder com um modelo especial projetado para AJAX, pode fazer isso da seguinte forma:

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

Trechos

A ferramenta mais poderosa oferecida pela Nette para conectar o servidor com o cliente são os snippets. Com eles, você pode transformar um aplicativo comum em um aplicativo AJAX com o mínimo de esforço e algumas linhas de código. O exemplo Fifteen demonstra como tudo isso funciona, e seu código pode ser encontrado no GitHub.

Os snippets, ou recortes, permitem que você atualize apenas partes da página, em vez de recarregar a página inteira. Isso é mais rápido e mais eficiente, além de proporcionar uma experiência de usuário mais confortável. Os snippets podem lembrá-lo do Hotwire para Ruby on Rails ou do Symfony UX Turbo. É interessante notar que a Nette introduziu os snippets 14 anos antes.

Como os snippets funcionam? Quando a página é carregada pela primeira vez (uma solicitação não AJAX), a página inteira, incluindo todos os snippets, é carregada. Quando o usuário interage com a página (por exemplo, clica em um botão, envia um formulário etc.), em vez de carregar a página inteira, é feita uma solicitação AJAX. O código no apresentador executa a ação e decide quais trechos precisam ser atualizados. A Nette renderiza esses trechos e os envia na forma de uma matriz JSON. O código de manipulação no navegador insere os trechos recebidos de volta na página. Portanto, somente o código dos trechos alterados é transferido, economizando largura de banda e acelerando o carregamento em comparação com a transferência de todo o conteúdo da página.

Naja

Para lidar com snippets no navegador, é usada a biblioteca Naja. Instale-a como um pacote node.js (para uso com aplicativos como Webpack, Rollup, Vite, Parcel e outros):

npm install naja

… ou insira-a diretamente no modelo da página:

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

Primeiro, você precisa inicializar a biblioteca:

naja.initialize();

Para transformar um link comum (sinal) ou o envio de um formulário em uma solicitação AJAX, basta marcar o respectivo link, formulário ou botão com a classe 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>

Redesenho de snippets

Todos os objetos da classe Control (inclusive o próprio Presenter) mantêm um registro da ocorrência de alterações que exijam o redesenho. O método redrawControl() é usado para essa finalidade.

public function handleLogin(string $user): void
{
	// depois de fazer login, é necessário redesenhar a parte relevante
	$this->redrawControl();
	//...
}

O Nette também permite um controle mais preciso do que precisa ser redesenhado. O método mencionado acima pode usar o nome do snippet como argumento. Assim, é possível invalidar (ou seja, forçar um redesenho) no nível da parte do modelo. Se o componente inteiro for invalidado, todos os trechos dele também serão redesenhados:

// invalida o trecho de "cabeçalho
$this->redrawControl('header');

Snippets em Latte

Usar snippets no Latte é extremamente fácil. Para definir uma parte do modelo como um snippet, basta envolvê-la nas tags {snippet} e {/snippet}:

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

O snippet cria um elemento <div> na página HTML com um id especialmente gerado. Ao redesenhar um snippet, o conteúdo desse elemento é atualizado. Portanto, quando a página é renderizada inicialmente, todos os snippets também devem ser renderizados, mesmo que inicialmente estejam vazios.

Você também pode criar um snippet com um elemento diferente de <div> usando um atributo n::

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

Áreas de snippet

Os nomes dos snippets também podem ser expressões:

{foreach $items as $id => $item}
	<li n:snippet="item-{$id}">{$item}</li>
{/foreach}

Dessa forma, obteremos vários snippets como item-0, item-1, etc. Se invalidássemos diretamente um snippet dinâmico (por exemplo, item-1), nada seria redesenhado. O motivo é que os snippets funcionam como verdadeiros trechos e somente eles são renderizados diretamente. Entretanto, no modelo, não há tecnicamente um trecho chamado item-1. Ele só aparece quando se executa o código ao redor do snippet, nesse caso, o loop foreach. Por isso, marcaremos a parte do modelo que precisa ser executada com a tag {snippetArea}:

<ul n:snippetArea="itemsContainer">
	{foreach $items as $id => $item}
		<li n:snippet="item-{$id}">{$item}</li>
	{/foreach}
</ul>

E redesenharemos tanto o snippet individual quanto toda a área abrangente:

$this->redrawControl('itemsContainer');
$this->redrawControl('item-1');

Também é essencial garantir que a matriz $items contenha apenas os itens que devem ser redesenhados.

Ao inserir outro modelo no modelo principal usando a tag {include}, que tem snippets, é necessário envolver novamente o modelo incluído em um snippetArea e invalidar o snippet e a área juntos:

{snippetArea include}
	{include 'included.latte'}
{/snippetArea}
{* incluído.latte *}
{snippet item}
	...
{/snippet}
$this->redrawControl('include');
$this->redrawControl('item');

Snippets em componentes

Você pode criar snippets em componentes, e o Nette os redesenha automaticamente. Entretanto, há uma limitação específica: para redesenhar os snippets, ele chama o método render() sem nenhum parâmetro. Portanto, a passagem de parâmetros no modelo não funcionará:

OK
{control productGrid}

will not work:
{control productGrid $arg, $arg}
{control productGrid:paginator}

Envio de dados do usuário

Juntamente com os snippets, você pode enviar quaisquer dados adicionais ao cliente. Basta gravá-los no objeto payload:

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

Parâmetros de envio

Quando enviamos parâmetros ao componente por meio de solicitação AJAX, sejam eles 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().

let url = new URL({link //foo!});
url.searchParams.set({$control->getParameterId('bar')}, bar);

fetch(url, {
	headers: {'X-Requested-With': 'XMLHttpRequest'},
})

Um método handle com os parâmetros correspondentes no componente:

public function handleFoo(int $bar): void
{
}
versão: 4.0