You are browsing the unmaintained documentation for old Nette 2.1. See documentation for current Nette.

AJAX & snippety

Moderní webové aplikace dnes běží napůl na serveru, napůl v prohlížeči. AJAX je tím klíčovým spojovacím prvkem. Jakou podporu nabízí Nette Framework?

  • posílání výřezů šablony (tzv. snippety)
  • předávání proměnných mezi PHP a JavaScriptem
  • debugování AJAXových aplikací

AJAXový požadavek lze detekovat metodou služby zapouzdřující HTTP požadavek $httpRequest->isAjax() (detekuje podle HTTP hlavičky X-Requested-With). Uvnitř presenteru je k dispozici „zkratka“ v podobě metody $this->isAjax().

AJAXový požadavek se nijak neliší od klasického požadavku – je zavolán presenter s určitým view a parametry. Je také věcí presenteru, jak bude na něj reagovat: může použít vlastní rutinu, která vrátí nějaký fragment HTML kódu (HTML snippet), XML dokument, JSON objekt nebo kód v JavaScriptu.

Upozornění: Nette neobsahuje implementaci AJAX požadavků na straně klienta. Doporučujeme knihovnu nette.ajax.js od Vojty Dobeše.

Pro odesílání dat prohlížeči ve formátu JSON lze využít předpřipravený objekt payload:

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

Pokud potřebujete plnou kontrolu nad odeslaným JSONem, použijte v presenteru JsonResponse. Tím ihned ukončíte činnost presenteru a obejdete se i bez šablony:

$this->sendResponse(new JsonResponse(array('klic' => 'hodnota', ...)));

Když chceme odeslat HTML, můžeme jednak zvolit speciální šablonu pro AJAX:

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

Nicméně daleko silnější nástroj představuje vestavěná podpora AJAXových snippetů. Díky ní lze udělat z obyčejné aplikace AJAXovou prakticky několika řádky kódu. Jak to celé funguje demonstruje příklad Fifteen, jehož kód najdete v distribuci.

Snippety fungují tak, že při prvotním (tedy neAJAXovém) požadavku se přenese celá stránka a poté se při každém již AJAXovém subrequestu (= požadavku na stejný presenter a view) přenáší pouze kód změněných částí ve zmíněném úložišti payload. K tomu slouží dva mechanismy: invalidace a renderování snippetů.

Invalidace

Každý objekt třídy Control (což je i samotný Presenter) si umí zapamatovat, jestli při subrequestu došlo ke změnám, které si vyžadují jej překreslit. K tomu slouží dvojice metod redrawControl() a isControlInvalid(). Příklad:

public function handleLogin($user)
{
    // po přihlášení uživatele se musí objekt překreslit
    $this->redrawControl();
    ...
}

Nette však nabízí ještě jemnější rozlišení, než na úrovni komponent. Uvedené metody mohou totiž jako argument přijímat název tzv. „snippetu“, nebo-li výstřižku. Lze tedy invalidovat (rozuměj: vynutit překreslení) na úrovni těchto snippetů (každý objekt může mít libovolné množství snippetů). Pokud se invaliduje celá komponenta, tak se i každý snippet překreslí. Komponenta je „invalidní“ i tehdy, pokud je invalidní některá její subkomponenta.

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

$this->redrawControl('header'); // invaliduje snippet 'header'
echo $this->isControlInvalid('header'); // -> TRUE
echo $this->isControlInvalid('footer'); // -> FALSE
echo $this->isControlInvalid(); // -> TRUE, alespoň jeden snippet je invalid

$this->redrawControl(); // invaliduje celou komponentu, každý snippet
echo $this->isControlInvalid('footer'); // -> TRUE

Komponenta, která přijímá signál, je automaticky označena za invalidní.

Díky invalidaci snippetů přesně víme, které části kterých prvků bude potřeba překreslit.

Makro {snippet} … {/snippet}

Nette je založeno na myšlence logických, nikoliv grafických prvků, tj. objekt třídy Control nepředstavuje pravoúhlou oblast ve stránce, ale logickou komponentu, která se může renderovat i do více podob (např. hypotetická komponenta DataGrid může mít jednu metodu pro vykreslení mřížky a druhou pro vykreslení „stránkovadla“ apod). Každý prvek může být navíc na stránce vykreslen vícekrát, nebo podmíněně, nebo pokaždé s jinou šablonou atd.

Není tedy možné jednoduše zavolat nějakou metodu render na každém invalidním objektu. K vykreslování je nutné přistupovat tak, jako když se kreslí celá stránka. Vykreslování stránky probíhá velmi podobně, jako u neAJAXového požadavku, načtou se tytéž šablony atd. Klíčovým úkolem však je vypustit ty části, které se na výstup vůbec dostat nemají, a ty, které se vykreslit mají, přidružit s identifikátorem a poslat klientovi ve formátu, kterému bude obslužný JavaScript rozumět.

Syntaxe

Pokud se uvnitř šablony nachází control nebo snippet, musíme jej obalit párovou značkou {snippet} ... {/snippet} – ty totiž zajistí, že se vykreslený snippet vystřihne a pošle do prohlížeče. Také jej obalí pomocnou značkou <div> s vygenerovaným id. V uvedeném příkladě je snippet pojmenován jako header a může představovat i například šablonu controlu:

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

Snippetu jiného typu než <div> nebo snippetu s dalšími HTML atributy docílíme nejsnáze použitím atributové varianty makra:

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

Dynamické snippety

Nette také umožňuje používání snippetů, jejichž název se vytvoří až za běhu – tj. dynamicky. Hodí se to pro různé seznamy, kde při změně jednoho řádku nechceme přenášet AJAXem celý seznam, ale stačí onen samotný řádek. Příklad:

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

Zde máme statický snippet itemsContainer, obsahující několik dynamických snippetů item-0, item-1 atd.

Dynamické snippety nelze invalidovat přímo (invalidace item-1 neudělá vůbec nic), musíte invalidovat jim nadřazený statický snippet (zde snippet itemsContainer). Potom dojde k tomu, že se provede celý kód toho kontejneru, ale prohlížeči se pošlou jenom jeho sub-snippety. Pokud chcete, aby prohlížeč dostal pouze jediný z nich, musíte upravit vstup toho kontejneru tak, aby ostatní negeneroval.

V příkladu výše zkrátka musíte zajistit, aby při ajaxovém požadavku byla v proměnné $list pouze jedna položka a tedy aby ten cyklus foreach naplnil pouze jeden dynamický snippet:

class HomepagePresenter extends \Nette\Application\UI\Presenter
{
    /**
     * Zde je nějaká logika pro získání celého seznamu. Správně to patří do
     * modelu, ale pro příklad to nebudu komplikovat.
     * @return array
     */
    private function getTheWholeList()
    {
        return array(
            'První',
            'Druhý',
            'Třetí'
        );
    }

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

    public function handleUpdate($id)
    {
        $this->template->list = $this->isAjax()
                ? array()
                : $this->getTheWholeList();
        $this->template->list[$id] = 'Updated item';
        $this->redrawControl('itemsContainer');
    }
}

Dynamické snippety v includované šabloně

Může se stát, že máte celý foreach s dynamickými snippety v šabloně, kterou teprve includujete do jiné šablony. V takovém případě musíte ještě v nadřazené šabloně obalit {include 'sablona.latte'} do speciálního makra {snippetArea itemsContainerArea}. Dynamický snippet se potom invaliduje stejně jako v předchozím příkladu, pouze je potřeba invalidovat navíc ještě snippetArea.

$this->redrawControl('itemsContainer');
$this->redrawControl('itemsContainerArea');

Přidávání a mazání

Pokud přidáte novou položku a invalidujete itemsContainer, pak vám AJAXový požadavek sice vrátí i nový snippet, ale obslužný javascript ho neumí nikam přiřadit. Na stránce totiž zatím není žádný HTML prvek s takovým ID.

V takovém případě je nejjednodušší celý ten seznam obalit ještě jedním snippetem a invalidovat to celé:

{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');
}

Totéž platí i pro mazání. Sice by se dal nějak poslat prázdný snippet, jenže v praxi jsou většinou seznamy stránkované a řešit úsporněji smazání jednoho plus případné načtení jiného (který se předtím nevešel) by bylo příliš složité.