AJAX & Snippets
Modern web applications nowadays run half on a server and half in a browser. AJAX is a vital uniting factor. What support does the Nette Framework offer?
- sending template fragments (so-called snippets)
- passing variables between PHP and JavaScript
- AJAX applications debugging
AJAX Request
An AJAX request does not differ from a classic request – the presenter is called with a specific view and parameters. It is also up to the presenter how to respond to it: it can use its own routine, which returns an HTML code fragment (HTML snippet), an XML document, a JSON object, or JavaScript code.
On the server side, an AJAX request can be detected using the service method encapsulating the HTTP request $httpRequest->isAjax()
(detects
based on the HTTP header X-Requested-With
). Inside the presenter, a shortcut is available in the form of the method
$this->isAjax()
.
There is a pre-processed object called payload
dedicated to sending data to the browser in JSON.
public function actionDelete(int $id): void
{
if ($this->isAjax()) {
$this->payload->message = 'Success';
}
// ...
}
For a full control over your JSON output use the sendJson
method in your presenter. It terminates presenter
immediately and you'll do without a template:
$this->sendJson(['key' => 'value', /* ... */]);
If we want to send HTML, we can either set a special template for AJAX requests:
public function handleClick($param): void
{
if ($this->isAjax()) {
$this->template->setFile('path/to/ajax.latte');
}
// ...
}
Naja
The Naja library is used to handle AJAX requests on the browser side. Install it as a node.js package (to use with Webpack, Rollup, Vite, Parcel and more):
npm install naja
…or insert it directly into the page template:
<script src="https://unpkg.com/naja@2/dist/Naja.min.js"></script>
To create an AJAX request from a regular link (signal) or form submittion, simply mark the relevant link, form, or button with
the class 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
There is a far more powerful tool of built-in AJAX support – snippets. Using them makes it possible to turn a regular application into an AJAX one using only a few lines of code. How it all works is demonstrated in the Fifteen example whose code is also accessible in the build or on GitHub.
The way snippets work is that the whole page is transferred during the initial (i.e. non-AJAX) request and then with every AJAX
subrequest (request of the same view of the same
presenter) only the code of the changed parts is transferred in the payload
repository mentioned earlier.
Snippets may remind you of Hotwire for Ruby on Rails or Symfony UX Turbo, but Nette came up with them fourteen years earlier.
Invalidation of Snippets
Each descendant of class Control (which a Presenter is too) is
able to remember whether there were any changes during a request that require it to re-render. There are a pair of methods to
handle this: redrawControl()
and isControlInvalid()
. An example:
public function handleLogin(string $user): void
{
// The object has to re-render after the user has logged in
$this->redrawControl();
// ...
}
Nette however offers an even finer resolution than whole components. The listed methods accept the name of a so-called “snippet” as an optional parameter. A “snippet” is basically an element in your template marked for that purpose by a Latte tag, more on that later. Thus it is possible to ask a component to redraw only parts of its template. If the entire component is invalidated then all of its snippets are re-rendered. A component is “invalid” also if any of its subcomponents is invalid.
$this->isControlInvalid(); // -> false
$this->redrawControl('header'); // invalidates the snippet named 'header'
$this->isControlInvalid('header'); // -> true
$this->isControlInvalid('footer'); // -> false
$this->isControlInvalid(); // -> true, at least one snippet is invalid
$this->redrawControl(); // invalidates the whole component, every snippet
$this->isControlInvalid('footer'); // -> true
A component which receives a signal is automatically marked for redrawing.
Thanks to snippet redrawing we know exactly which parts of which elements should be re-rendered.
Tag {snippet} … {/snippet}
Rendering of the page proceeds very similarly to a regular request: the same templates are loaded, etc. The vital part is, however, to leave out the parts that are not supposed to reach the output; the other parts shall be associated with an identifier and sent to the user in a comprehensible format for a JavaScript handler.
Syntax
If there is a control or a snippet in the template, we have to wrap it using the {snippet} ... {/snippet}
pair
tag – it will make sure that the rendered snippet will be “cut out” and sent to the browser. It will also enclose it in a
helper <div>
tag (it is possible to use a different one). In the following example a snippet named
header
is defined. It may as well represent the template of a component:
{snippet header}
<h1>Hello ... </h1>
{/snippet}
A snippet of a type other than <div>
or a snippet with additional HTML attributes is achieved by using the
attribute variant:
<article n:snippet="header" class="foo bar">
<h1>Hello ... </h1>
</article>
Dynamic Snippets
In Nette you can also define snippets with a dynamic name based on a runtime parameter. This is most suitable for various lists where we need to change just one row but we don't want transfer the whole list along with it. An example of this would be:
<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>
There is one static snippet called itemsContainer
, containing several dynamic snippets: item-0
,
item-1
and so on.
You can't redraw a dynamic snippet directly (redrawing of item-1
has no effect), you have to redraw its parent
snippet (in this example itemsContainer
). This causes the code of the parent snippet to be executed, but then just
its sub-snippets are sent to the browser. If you want to send over just one of the sub-snippets, you have to modify input for the
parent snippet to not generate the other sub-snippets.
In the example above you have to make sure that for an AJAX request only one item will be added to the $list
array, therefore the foreach
loop will print just one dynamic snippet.
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 in an Included Template
It can happen that the snippet is in a template which is being included from a different template. In that case we need to wrap
the inclusion code in the second template with the snippetArea
tag, then we redraw both the snippetArea and the
actual snippet.
Tag snippetArea
ensures that the code inside is executed but only the actual snippet in the included template is
sent to the browser.
{* parent.latte *}
{snippetArea wrapper}
{include 'child.latte'}
{/snippetArea}
{* child.latte *}
{snippet item}
...
{/snippet}
$this->redrawControl('wrapper');
$this->redrawControl('item');
You can also combine it with dynamic snippets.
Adding and Deleting
If you add a new item into the list and invalidate itemsContainer
, the AJAX request returns snippets including the
new one, but the javascript handler won’t be able to render it. This is because there is no HTML element with the newly
created ID.
In this case, the simplest way is to wrap the whole list in one more snippet and invalidate it all:
{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');
}
The same goes for deleting an item. It would be possible to send empty snippet, but usually lists can be paginated and it would be complicated to implement deleting one item and loading another (which used to be on a different page of the paginated list).
Sending Parameters to Component
When we send parameters to the component via AJAX request, whether signal parameters or persistent parameters, we must provide
their global name, which also contains the name of the component. The full name of parameter returns the
getParameterId()
method.
$.getJSON(
{link changeCountBasket!},
{
{$control->getParameterId('id')}: id,
{$control->getParameterId('count')}: count
}
});
And handle method with s corresponding parameters in component.
public function handleChangeCountBasket(int $id, int $count): void
{
}