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