Edit
Lang

AJAX & Snippets

Modern web applications nowadays run half on a server and half in a browser. AJAX is the vital uniting factor. What support does 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.

Caution: Nette doesn't implement client-side AJAX requests. We recommend nette.ajax.js library by Vojtěch Dobeš 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 JsonResponse in your presenter. It terminates presenter immediately and you'll do without a template:

$this->sendResponse(new JsonResponse(['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.phtml');
    }
    ...
}

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 unsing 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 other AJAX subrequest (request of the same view of the same presenter) only the code of the changed parts is transferred in the earlier mentioned repository called payload. There are two mechanisms designated for this: invalidation and snippets rendering.

Invalidation

Each descendant of class Control (which a Presenter is too) is able to remember whether there were any changes during a subrequest that require it to re-render. There is 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 slighter distinction than on the level of components. The listed methods can accept a name of a snippet as a parameter. Hence, it is possible to invalidate (that is to force to re-render) on the level of snippets (every object can keep any number of snippets). If the entire component is invalidated, then every snippet re-renders. A component is “invalid” even if any of its subcomponents is invalid as well.

echo $this->isControlInvalid(); // -> FALSE

$this->redrawControl('header'); // invalidates the snippet named 'header'
echo $this->isControlInvalid('header'); // -> TRUE
echo $this->isControlInvalid('footer'); // -> FALSE
echo $this->isControlInvalid(); // -> TRUE, at least one snippet is invalid

$this->redrawControl(); // invalidates the whole component, every snippet
echo $this->isControlInvalid('footer'); // -> TRUE

A component that receives a signal, is automatically invalidated.

Thanks to invalidation of snippets we know exactly which parts of which elements will it be necessary to re-render.

Macro {snippet} … {/snippet}

Nette is based on the idea of logical, not graphical elements, i.e. a descendant of Control does not represent a rectangular area on a page, but a logical component that that can render even into various forms. (for instance a hypothetical component DataGrid can have a method for rendering the grid and another one for rendering the “paginator”, and so on). Each element can also be rendered multiple times on a single page; or conditionally, or every time with a different template, etc. Therefore it is not possible to call some render method of every invalid object. The approach to the render must be the same as when the whole page is being rendered.

Rendering of the page proceeds very similarly as in case of 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 and those that are shall be associated with an identifier and be 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 is named header and may as well represent a template of a control:

{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 use snippets, whose name is created during runtime. This is best suitable for various lists where we need to change just one row, but we don't want transfer the whole list by AJAX. 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 invalidate dynamic snippet directly (invalidation of item-1 takes no effect), you have to invalidate its parent snippet (in this example itemsContainer). This causes all code for the parent snippet to be executed, but 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 to a different template. In such case we need to wrap the inclusion of a template in the snippetArea macro, then we invalidate both 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 do 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).