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
Solicitação AJAX
Uma solicitação AJAX não difere de uma solicitação clássica – o apresentador é chamado com uma visão e parâmetros específicos. Cabe também ao apresentador como responder a ela: ele pode usar sua própria rotina, que retorna um fragmento de código HTML (HTML snippet), um documento XML, um objeto JSON ou código JavaScript.
No lado do servidor, uma solicitação AJAX pode ser detectada usando o método de serviço que encapsula a solicitação HTTP $httpRequest->isAjax()
(detecta com base no cabeçalho HTTP X-Requested-With
). Dentro do apresentador, um atalho está disponível na forma
do método $this->isAjax()
.
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>
Para criar uma solicitação AJAX a partir de um link regular (sinal) ou envio de formulário, basta marcar o link,
formulário ou botão relevante 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>
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
{
}