AJAX & Snippets
Na era das aplicações web modernas, onde a funcionalidade é frequentemente dividida entre o servidor e o navegador, o AJAX é um elemento de ligação essencial. Que opções o Nette Framework nos oferece nesta área?
- envio de partes do template, os chamados snippets
- passagem de variáveis entre PHP e JavaScript
- ferramentas para depuração de requisições AJAX
Requisição AJAX
Uma requisição AJAX, em princípio, não difere de uma requisição HTTP clássica. Um presenter é chamado com determinados parâmetros. E cabe ao presenter decidir como responder à requisição – ele pode retornar dados em formato JSON, enviar uma parte do código HTML, um documento XML, etc.
No lado do navegador, inicializamos a requisiçã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, reconhecemos uma requisição AJAX usando o método $httpRequest->isAjax()
do serviço encapsulando a requisição HTTP. Para a deteção, ele usa o cabeçalho HTTP
X-Requested-With
, por isso é importante enviá-lo. Dentro do presenter, pode-se usar o método
$this->isAjax()
.
Se desejar enviar dados em formato JSON, use o método sendJson()
. O método também
encerra a atividade do presenter.
public function actionExport(): void
{
$this->sendJson($this->model->getData);
}
Se você planeja responder com um template especial destinado ao AJAX, pode fazê-lo da seguinte forma:
public function handleClick($param): void
{
if ($this->isAjax()) {
$this->template->setFile('path/to/ajax.latte');
}
// ...
}
Snippets
O recurso mais poderoso que o Nette oferece para conectar o servidor ao cliente são os snippets. Graças a eles, você pode transformar uma aplicação comum em uma aplicação AJAX com esforço mínimo e algumas linhas de código. O exemplo Fifteen demonstra como tudo funciona, e seu código pode ser encontrado no GitHub.
Snippets, ou trechos, permitem atualizar apenas partes da página, em vez de recarregar a página inteira. Isso não só é mais rápido e eficiente, mas também proporciona 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. Curiosamente, o Nette introduziu os snippets 14 anos antes.
Como os snippets funcionam? No primeiro carregamento da página (requisição não-AJAX), a página inteira é carregada, incluindo todos os snippets. 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, uma requisição AJAX é disparada. O código no presenter executa a ação e decide quais snippets precisam ser atualizados. O Nette renderiza esses snippets e os envia como um array em formato JSON. O código de manipulação no navegador insere os snippets recebidos de volta na página. Assim, apenas o código dos snippets alterados é transmitido, economizando largura de banda e acelerando o carregamento em comparação com a transferência do conteúdo da página inteira.
Naja
Para manipular snippets no lado do navegador, utiliza-se a biblioteca Naja. Instale-a como um pacote node.js (para uso com aplicações Webpack, Rollup, Vite, Parcel e outras):
npm install naja
…ou insira-a diretamente no template da página:
<script src="https://unpkg.com/naja@2/dist/Naja.min.js"></script>
Primeiro, é necessário inicializar a biblioteca:
naja.initialize();
Para transformar um link comum (sinal) ou o envio de um formulário em uma requisição AJAX, basta marcar o link,
formulário ou botão correspondente com a classe ajax
:
<a n:href="go!" class="ajax">Go</a>
<form n:name="form" class="ajax">
<input n:name="submit">
</form>
ou
<form n:name="form">
<input n:name="submit" class="ajax">
</form>
Redesenho de snippets
Cada objeto da classe Control (incluindo o próprio Presenter)
regista se ocorreram alterações que exigem o seu redesenho. Para isso, serve o método redrawControl()
:
public function handleLogin(string $user): void
{
// após o login, é necessário redesenhar a parte relevante
$this->redrawControl();
// ...
}
O Nette permite um controlo ainda mais fino do que deve ser redesenhado. O método mencionado pode receber o nome do snippet como argumento. Assim, é possível invalidar (entenda-se: forçar o redesenho) ao nível das partes do template. Se todo o componente for invalidado, cada um dos seus snippets também será redesenhado:
// invalida o snippet 'header'
$this->redrawControl('header');
Snippets em Latte
Usar snippets em Latte é extremamente fácil. Para definir uma parte do template como um snippet, basta envolvê-la com as
tags {snippet}
e {/snippet}
:
{snippet header}
<h1>Olá ... </h1>
{/snippet}
O snippet cria um elemento <div>
na página HTML com um id
especial gerado. Ao redesenhar
o snippet, o conteúdo desse elemento é atualizado. Por isso, é necessário que, na renderização inicial da página, todos
os snippets também sejam renderizados, mesmo que possam estar vazios no início.
Você também pode criar um snippet com um elemento diferente de <div>
usando o n:atributo:
<article n:snippet="header" class="foo bar">
<h1>Olá ... </h1>
</article>
Áreas de Snippets
Os nomes dos snippets também podem ser expressões:
{foreach $items as $id => $item}
<li n:snippet="item-{$id}">{$item}</li>
{/foreach}
Assim, teremos vários snippets item-0
, item-1
, etc. Se invalidássemos diretamente um snippet
dinâmico (por exemplo, item-1
), nada seria redesenhado. A razão é que os snippets funcionam realmente como
recortes e apenas eles próprios são renderizados diretamente. No entanto, no template, não existe de facto nenhum snippet
chamado item-1
. Ele só surge com a execução do código ao redor do snippet, ou seja, o ciclo foreach. Portanto,
marcamos a parte do template que deve ser executada usando a tag {snippetArea}
:
<ul n:snippetArea="itemsContainer">
{foreach $items as $id => $item}
<li n:snippet="item-{$id}">{$item}</li>
{/foreach}
</ul>
E mandamos redesenhar tanto o snippet em si quanto toda a área pai:
$this->redrawControl('itemsContainer');
$this->redrawControl('item-1');
Ao mesmo tempo, é aconselhável garantir que o array $items
contenha apenas os itens que devem ser
redesenhados.
Se incluirmos outro template que contém snippets no template usando a tag {include}
, é necessário envolver a
inclusão do template novamente em snippetArea
e invalidá-la junto com o snippet:
{snippetArea include}
{include 'included.latte'}
{/snippetArea}
{* included.latte *}
{snippet item}
...
{/snippet}
$this->redrawControl('include');
$this->redrawControl('item');
Snippets em Componentes
Você também pode criar snippets em componentes e o Nette irá
redesenhá-los automaticamente. Mas existe uma certa limitação: para redesenhar os snippets, ele chama o método
render()
sem parâmetros. Portanto, a passagem de parâmetros no template não funcionará:
OK
{control productGrid}
não funcionará:
{control productGrid $arg, $arg}
{control productGrid:paginator}
Envio de dados do usuário
Juntamente com os snippets, você pode enviar quaisquer outros dados para o cliente. Basta escrevê-los no objeto
payload
:
public function actionDelete(int $id): void
{
// ...
if ($this->isAjax()) {
$this->payload->message = 'Sucesso';
}
}
Passagem de parâmetros
Se enviarmos parâmetros para um componente através de uma requisição AJAX, sejam parâmetros de sinal ou parâmetros
persistentes, devemos indicar na requisição o seu nome global, que também inclui o nome do componente. O nome completo do
parâmetro é retornado pelo método getParameterId()
.
let url = new URL({link //foo!});
url.searchParams.set({$control->getParameterId('bar')}, bar);
fetch(url, {
headers: {'X-Requested-With': 'XMLHttpRequest'},
})
E o método handle com os parâmetros correspondentes no componente:
public function handleFoo(int $bar): void
{
}