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.
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 metodu sendJson
v presenteru. Tím ihned
ukončíte činnost presenteru a obejdete se i bez šablony:
$this->sendJson(['key' => 'value', /* ... */]);
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');
}
// ...
}
Naja
K obsluze AJAXových požadavků na straně prohlížeče slouží knihovna Naja, kterou si nainstalujte jako node.js balíček (pro použití s aplikacemi Webpack, Rollup, Vite, Parcel a dalšími):
npm install naja
…nebo přímo vložte do šablony stránky:
<script src="https://unpkg.com/naja@2/dist/Naja.min.js"></script>
Snippety
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 na GitHubu.
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 snippetů
Každý objekt třídy Control (což je i samotný
Presenter) si umí zapamatovat, jestli při signálu 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.
$this->isControlInvalid(); // -> false
$this->redrawControl('header'); // invaliduje snippet 'header'
$this->isControlInvalid('header'); // -> true
$this->isControlInvalid('footer'); // -> false
$this->isControlInvalid(); // -> true, alespoň jeden snippet je invalid
$this->redrawControl(); // invaliduje celou komponentu, každý snippet
$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.
Značky {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:
<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 [
'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()
? []
: $this->getTheWholeList();
$this->template->list[$id] = 'Updated item';
$this->redrawControl('itemsContainer');
}
}
Snippety v includované šabloně
Může se stát, že máme snippet v šabloně, kterou teprve includujeme do jiné šablony. V takovém případě je nutné
vkládání této šablony obalit značkami snippetArea
, které pak invalidujeme spolu se samotnym snippetem.
Značky snippetArea
zaručí, že se daný kód, který vkládá šablonu, provede, do prohlížeče se však
odešle pouze snippet v includované šabloně.
{* parent.latte *}
{snippetArea wrapper}
{include 'child.latte'}
{/snippetArea}
{* child.latte *}
{snippet item}
...
{/snippet}
$this->redrawControl('wrapper');
$this->redrawControl('item');
Tento přístup se nechá použít i v kombinaci s dynamickými snippety.
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é.
Posílání parametrů do komponenty
Pokud komponentě pomocí AJAXového požadavku odesíláme parametry, ať už parametry signálu nebo persistentní parametry,
musíme u požadavku uvést jejich globální název, který obsahuje i jméno komponenty. Celý název parametru vrací metoda
getParameterId()
.
$.getJSON(
{link changeCountBasket!},
{
{$control->getParameterId('id')}: id,
{$control->getParameterId('count')}: count
}
});
A handle metoda s odpovídajícími parametry v komponentě.
public function handleChangeCountBasket($id, $count)
{
}