AJAX & Snippets

In the era of modern web applications, where functionality often spans between the server and the browser, AJAX is an essential connecting element. What options does the Nette Framework offer in this area?

  • sending parts of the template, so-called snippets
  • passing variables between PHP and JavaScript
  • tools for debugging AJAX requests

AJAX Request

An AJAX request fundamentally does not differ from a classic HTTP request. A presenter is called with specific parameters. It's up to the presenter how to respond to the request – it can return data in JSON format, send a part of HTML code, an XML document, etc.

On the browser side, we initiate an AJAX request using the fetch() function:

fetch(url, {
	headers: {'X-Requested-With': 'XMLHttpRequest'},
})
.then(response => response.json())
.then(payload => {
	// processing the response
});

On the server side, an AJAX request is recognized by the $httpRequest->isAjax() method of the service encapsulating the HTTP request. It uses the HTTP header X-Requested-With, so it's essential to send it. Within the presenter, you can use the $this->isAjax() method.

If you want to send data in JSON format, use the sendJson() method. The method also terminates the presenter's activity.

public function actionExport(): void
{
	$this->sendJson($this->model->getData);
}

If you plan to respond with a special template designed for AJAX, you can do it as follows:

public function handleClick($param): void
{
	if ($this->isAjax()) {
		$this->template->setFile('path/to/ajax.latte');
	}
	// ...
}

Snippets

The most powerful tool offered by Nette for connecting the server with the client are snippets. With them, you can turn an ordinary application into an AJAX one with minimal effort and a few lines of code. The Fifteen example demonstrates how it all works, and its code can be found on GitHub.

Snippets, or clippings, allow you to update only parts of the page, instead of reloading the entire page. This is faster and more efficient, and also provides a more comfortable user experience. Snippets might remind you of Hotwire for Ruby on Rails or Symfony UX Turbo. Interestingly, Nette introduced snippets 14 years earlier.

How do snippets work? When the page is first loaded (a non-AJAX request), the entire page, including all snippets, is loaded. When the user interacts with the page (e.g., clicks a button, submits a form, etc.), instead of loading the entire page, an AJAX request is made. The code in the presenter performs the action and decides which snippets need updating. Nette renders these snippets and sends them in the form of a JSON array. The handling code in the browser then inserts the received snippets back into the page. Therefore, only the code of the changed snippets is transferred, saving bandwidth and speeding up loading compared to transferring the entire page content.

Naja

To handle snippets on the browser side, the Naja library is used. Install it as a node.js package (for use with applications such as Webpack, Rollup, Vite, Parcel, and others):

npm install naja

… or insert it directly into the page template:

<script src="https://unpkg.com/naja@2/dist/Naja.min.js"></script>

To make an ordinary link (signal) or form submission an AJAX request, simply mark the respective link, form, or button with the ajax class:

<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>

Redrawing Snippets

Every object of the Control class (including the Presenter itself) keeps a record of whether changes have occurred that necessitate its redrawing. The redrawControl() method is employed for this purpose.

public function handleLogin(string $user): void
{
	// after logging in, it is necessary to redraw the relevant part
	$this->redrawControl();
	// ...
}

Nette also allows for finer control of what needs redrawing. The aforementioned method can take the snippet name as an argument. Thus, it's possible to invalidate (meaning: force a redraw) at the template part level. If the entire component is invalidated, every snippet of it is also redrawn:

// invalidates the 'header' snippet
$this->redrawControl('header');

Snippets in Latte

Using snippets in Latte is extremely easy. To define a part of the template as a snippet, simply wrap it in {snippet} and {/snippet} tags:

{snippet header}
	<h1>Hello ... </h1>
{/snippet}

The snippet creates an element <div> in the HTML page with a specially generated id. When redrawing a snippet, the content of this element is updated. Therefore, when the page is initially rendered, all snippets must also be rendered, even if they may initially be empty.

You can also create a snippet with an element other than <div> using an n:attribute:

<article n:snippet="header" class="foo bar">
	<h1>Hello ... </h1>
</article>

Snippet Areas

Snippet names can also be expressions:

{foreach $items as $id => $item}
	<li n:snippet="item-{$id}">{$item}</li>
{/foreach}

This way, we'll get several snippets like item-0, item-1, etc. If we were to directly invalidate a dynamic snippet (e.g., item-1), nothing would be redrawn. The reason being, snippets function as true excerpts and only they themselves are rendered directly. However, in the template, there isn't technically a snippet named item-1. It only emerges when executing the surrounding code of the snippet, in this case, the foreach loop. Hence, we'll mark the part of the template that needs to be executed with the {snippetArea} tag:

<ul n:snippetArea="itemsContainer">
	{foreach $items as $id => $item}
		<li n:snippet="item-{$id}">{$item}</li>
	{/foreach}
</ul>

And we'll redraw both the individual snippet and the entire overarching area:

$this->redrawControl('itemsContainer');
$this->redrawControl('item-1');

It's also essential to ensure that the $items array contains only the items that should be redrawn.

When inserting another template into the main one using the {include} tag, which has snippets, it's necessary to again wrap the included template in a snippetArea and invalidate both the snippet and the area together:

{snippetArea include}
	{include 'included.latte'}
{/snippetArea}
{* included.latte *}
{snippet item}
	...
{/snippet}
$this->redrawControl('include');
$this->redrawControl('item');

Snippets in Components

You can create snippets within components, and Nette will automatically redraw them. However, there's a specific limitation: to redraw snippets, it calls the render() method without any parameters. Thus, passing parameters in the template won't work:

OK
{control productGrid}

will not work:
{control productGrid $arg, $arg}
{control productGrid:paginator}

Sending User Data

Along with snippets, you can send any additional data to the client. Simply write them into the payload object:

public function actionDelete(int $id): void
{
	// ...
	if ($this->isAjax()) {
		$this->payload->message = 'Success';
	}
}

Sending Parameters

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.

let url = new URL({link //foo!});
url.searchParams.set({$control->getParameterId('bar')}, bar);

fetch(url, {
	headers: {'X-Requested-With': 'XMLHttpRequest'},
})

A handle method with the corresponding parameters in the component:

public function handleFoo(int $bar): void
{
}
version: 4.0 3.x 2.x