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
{

}
version: 4.0 3.x 2.x