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

An AJAX request can be detected using a method of a service encapsulating a HTTP request $httpRequest->isAjax() (detects based on the X-Requested-With HTTP header). There is also a shorthand method in presenter: $this->isAjax().

An AJAX request is no different from a normal one – a presenter is called with a certain view and parameters. It is, too, up to the presenter how will it react: it can use its routines to either return a fragment of HTML code (a snippet), an XML document, a JSON object or a piece of Javascript code.

Nette doesn't implement client-side AJAX requests. We recommend library Naja or Nittro framework.

There is a pre-processed object called payload dedicated to sending data to the browser in JSON.

public function actionDelete($id)
{
	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)
{
	if ($this->isAjax()) {
		$this->template->setFile('path/to/ajax.latte');
	}
	// ...
}

However, 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.

Invalidation

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($user)
{
	// 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 macro, 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}

If you would like to create a snippet with a different containing element than <div> or add custom attributes to the element, you can use the following definition:

<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 HomepagePresenter 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.
	 * @return array
	 */
	private function getTheWholeList()
	{
		return [
			'First',
			'Second',
			'Third'
		];
	}

	public function renderDefault()
	{
		if (!isset($this->template->list)) {
			$this->template->list = $this->getTheWholeList();
		}
	}

	public function handleUpdate($id)
	{
		$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 macro, then we redraw both the snippetArea and the actual snippet.

Macro 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()
{
	$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($id, $count)
{

}