Obsah
Návštěvní kniha využívající AJAX
Následující tutoriál Vás provede tvorbou jednoduché a nenáročné návštěvní knihy a zasvětí Vás při tom do světa AJAXu v Nette s pomocí jQuery.
Požadavky
- základní znalosti Nette a dibi
- prostředí vhodné k vývoji a běhu aplikací v Nette
- zhruba 30 – 60 minut času
Příprava
Naší návštěvní knihu nebudeme psát úplně od základů, pomůžeme si
kostrou aplikace, která je dostupná v distribučním balíku s Nette.
Nachází se ve složce tools/Skeleton a obsahuje předpřipravenou
adresářovou strukturu, několik základních tříd a dalších souborů,
které nám usnadní práci.
Příprava skeletonu je zde popsána jen stručně a pro úplnost, větší popis je obsahem jiných tutoriálů.
Začneme vytvořením složky v adresáři přístupném z testovacího
webového serveru a rozbalíme do ní obsah skeletonu. Do složky
libs nakopírujeme Nette a dibi. Dále do složky
document_root/js nakopírujeme jQuery.
A abychom si tu obsluhu AJAXu v jQuery nemuseli psát sami, využijeme již
připravených skriptů: jquery.nette.js a jquery.ajaxform.js – s nimi do
stejné složky, jako s jQuery.
Také bude dobré si ihned JavaScriptové knihovny do stránky nalinkovat,
ať na to později nezapomeneme. Do hlavičky v souboru
app/templates/@layout.phtml přidáme:
<script type="text/javascript" src="{$basePath}/js/jquery.js"></script>
<script type="text/javascript" src="{$basePath}/js/jquery.nette.js"></script>
<script type="text/javascript" src="{$basePath}/js/jquery.ajaxform.js"></script>
Aby naše návštěvní kniha vypadala alespoň trošku k světu, stáhneme
si mírně upravený soubor screen.css a
umístíme jej do složky document_root/css.
Protože budeme psát návštěvní knihu, bude také moudré si připravit
nějakou tu databázi. Použijeme SQLite, které je dostupné téměř vždy a
všude. Databáze to bude opravdu jednoduchá – vystačíme si s jedinou
tabulkou entries:
CREATE TABLE [entries] (
[id] INTEGER NOT NULL PRIMARY KEY,
[author] VARCHAR(50) NOT NULL,
[posted] TIMESTAMP NOT NULL,
[ip] VARCHAR(15) NOT NULL,
[text] TEXT NULL
);
CREATE INDEX [IDX_ENTRIES_POSTED] ON [entries] (
[posted] ASC
);
Celou databázi si můžete stáhnout: database.sdb.
Umístěte do složky app/models.
Poskytnutá databáze je ve formátu SQLite 2. Můžete si
stáhnout i databázi ve formátu SQLite
3. Poté ale nesmíte zapomenout použít v dibi driver
sqlite3. Pokud preferujete MySQL, je k dispozici i export pro
MySQL.
Nezapomeňte, že pokud chcete, aby kniha návštěv fungovala,
musí mít webserver oprávnění zapisovat nejen do souboru s databází, ale
i do složky, ve které je tato databáze umístěna – v našem případě
složka app/models. Pokud pracujete na systému, který vychází
z unixu, zvažte použití příkazu chmod -R a+rwX
app/models.
Začínáme
Nyní se již od kopírování a rozbalování knihoven můžeme pustit do samotné tvorby. Abychom demonstrovali jednoduchost a sílu AJAXu v Nette, vytvoříme nejdříve aplikaci bez jeho použití a až poté přidáme AJAX – se zachováním stejné funkčnosti.
Začneme tedy vytvořením jednoduchého modelu a připojením k databázi.
Modely a databáze
Ačkoliv v tomto tutoriálu budeme pracovat jen s jedinou tabulkou a tím
pádem si vystačíme s jediným modelem (a tedy jedinou třídou), vytvoříme
si modely dva: abstraktní BaseModel, který poslouží jako
šablona pro další modely (co když bude potřeba zítra do aplikace přidat
další funkce?), a EntriesModel, který bude reprezentovat
samotnou tabulku entries v databázi.
Modely
Jak již bylo řečeno, BaseModel poslouží jako kostra pro
další modely. Bude obsahovat nejen několik základních funkcí pro práci
s danou tabulkou, ale také se nám postará o připojování k databáze a
odpojování od ní. Vytvoříme si následující soubor
app/models/BaseModel.php:
<?php
abstract class BaseModel extends Object
{
/********************* Connection handling *********************/
/** @var DibiConnection */
public static $defaultConnection;
/**
* Establishes the database connection.
*/
public static function connect()
{
// use configuration from config.ini
self::$defaultConnection = dibi::connect(Environment::getConfig('database'));
}
/**
* Disconnects from the database.
*/
public static function disconnect()
{
self::$defaultConnection->disconnect();
}
/********************* Model behaviour *********************/
/** @var DibiConnection */
protected $connection;
/** @var string object name */
protected $name;
/** @var string primary key name */
protected $primary;
/** @var bool autoincrement? */
protected $autoIncrement = TRUE;
public function __construct(DibiConnection $connection = NULL)
{
$this->connection = ($connection !== NULL ? $connection : self::$defaultConnection);
}
/**
* Selects rows from the table in specified order
* @param array $order
* @return DibiResult
*/
public function fetchAll(array $order = array())
{
return $this->connection->query(
'SELECT * FROM %n', $this->name,
'%ex', (!empty($order) ? array('ORDER BY %by', $order) : NULL)
);
}
/**
* Inserts a new row
* @param array $values to insert
* @return
*/
public function insert(array $values)
{
return $this->connection->insert($this->name, $values)
->execute($this->autoIncrement ? dibi::IDENTIFIER : NULL);
}
}
Celá třída obsahuje jen ty funkce, které budeme pro náš příklad potřebovat. Jistě by se našlo místo na další funkce (aktualizace záznamů, složitější vybírání z databáze, …), ne však v tomto tutoriálu.
Třída se může na první pohled zdát složitá, po bližším
prozkoumání však o nic složitého nejde. Navíc oč složitější je tato
třída, o to jednodušší budou další modely.
app/models/EntriesModel.php bude vypadat takto:
<?php
class EntriesModel extends BaseModel
{
protected $name = 'entries';
}
Obsahuje jen definici názvu tabulky. Nic dalšího potřeba skutečně není.
Připojení k databázi
Metoda BaseModel::connect() nám sice umožňuje připojit se
k databázi, musíme jí ale někde zavolat a také musíme do
config.ini zapsat údaje pro připojení.
Začneme tedy těmi údaji. Do souboru app/config.ini přidáme
před začátek sekce [production < common]
následující řádky:
[common.database]
driver = sqlite
database = %appDir%/models/database.sdb
Pokud chcete použít databázi ve formátu SQLite 3, použijte
driver sqlite3. Formáty nejsou zaměnitelné, takže nelze
načíst SQLite 2 databázi pomocí driveru sqlite3 a naopak.
Pokud zatím s dibi moc nekamarádíte, můžete se také podívat na příklad
konfigurace pro MySQL.
To nám vytvoří podsekci konfigurace s názvem database
platnou pro všechna prostředí.
Když už máte otevřený soubor config.ini,
povšimněte si bezpečnostního
varování.
Nyní už zbývá se k databázi připojit. Toho dosáhneme pomocí
událostí aplikace. Do souboru app/bootstrap.php přidáme před
volání $application->run(); následující řádky kódu:
$application->onStartup[] = 'BaseModel::connect';
$application->onShutdown[] = 'BaseModel::disconnect';
Tím aplikaci nadefinujeme, že se má během svého spouštění připojit
k databázi voláním metody BaseModel::connect() a při
ukončování se zase slušně odpojit voláním
BaseModel::disconnect().
A to je k databázi vše. Pokud nyní při otevření aplikace v prohlížeči nespatříte chybové hlášení, aplikace je připravena pracovat s databází.
Výpis příspěvků a jejich přidávání
Návštěvní kniha většinou obsahuje jen jednu jedinou stránku – pro
výpis a současně i přidávání příspěvků. To nám situaci
zjednodušuje a můžeme pracovat na poli jediného presenteru –
HomepagePresenteru. V našem případě by měl obsahovat několik
základních částí:
- získání seznamu příspěvků z databáze
- vykreslení seznamu v šabloně
- definice formuláře a zpracování jeho dat
- vykreslení formuláře v šabloně
Začneme seznamem…
Seznam příspěvků
Abychom se dostali k seznamu příspěvků, musíme použít model. Máme několik možností, jak model vytvářet:
- vytvořit si jednu instanci modelu v metodě
startuppresetneru a tu používat - vytvořit si novou instanci modelu při každém použití v presenteru
- vytvořit si chytře jedinou instanci modelu při prvním použití a tu pak používat i později
Poslední způsob je asi nejelegantnější, použijeme proto ten. Jmenuje se
lazy loading. Vytvoříme si jednoduchou funkci (getter), která bude
kontrolovat, zda je daná členská proměnná NULL. Pokud ano, tak
vytvoří novou instanci modelu. Na konci tuto proměnnou vrátí. Třída
HomepagePresenter v souboru
app/presenters/HomepagePresenter.php bude vypadat takto:
<?php
class HomepagePresenter extends BasePresenter
{
/** @var EntriesModel */
protected $entriesModel;
/**
* Lazy getter for EntriesModel
* @return EntriesModel
*/
public function getEntries()
{
if ($this->entriesModel === NULL)
$this->entriesModel = new EntriesModel();
return $this->entriesModel;
}
}
V presenteru pak budeme používat členskou proměnnou
$entries – díky taťkovi
všech objektů se bude volat náš getter getEntries().
Zde to může působit trochu jako kanón na mouchy, ale je dobré si na podobné konstrukce zvyknout – líné vytváření objektů je velmi výhodné u větších aplikací. Pokud budeme mít modelů více a budeme s nimi muset pracovat z více tříd, pak budeme muset najít nějaké lepší a pohodlnější řešení. Pro jeden model však zůstaneme u této relativně jednoduché metody.
Budeme pokračovat metodou renderDefault(), která data
z databáze načte a připraví je šabloně:
Pokud si nejste jistí, proč zrovna
renderDefault(), konzultujte dokumentaci či jiný tutoriál.
class HomepagePresenter extends BasePresenter
{
/* .... */
/********************* Default view *********************/
public function renderDefault()
{
$this->template->entries = $this->entries->fetchAll(array('posted' => dibi::DESC));
}
}
A nakonec samotná šablona. Soubor
app/templates/Homepage/default.phtml upravíme takto:
{block content}
<h1>Kniha návštěv</h1>
<div class="list">
{if count($entries) > 0}
{foreach $entries as $entry}
<div class="entry">
<div class="author">{$entry->author}</div>
<div class="text">{!$entry->text|escape|nl2br}</div>
<div class="posted">{$entry->posted}</div>
</div>
{/foreach}
{else}
<div class="notice">Kniha návštěv zatím neobsahuje žádné příspěvky.</div>
{/if}
</div>
Pokud jsme nikde neudělali chybu, bude výstup nyní následující:

Seznam je prázdný, ale aby ho mohl někdo naplnit, musí mít jak.
Formulář pro přidávání příspěvků
Abychom mohli nějaký formulář v šabloně vykreslit, musíme jej
nejdříve nadefinovat. To uděláme opět ve třídě
HomepagePresenter, vytvoříme si na formulář továrničku.
Ta nám formulář vytvoří až v momentě, kdy je ho skutečně potřeba.
U presenterů s mnoha komponentami a mnoha pohledy by bylo nákladné
vytvářet vždy všechny komponenty a pracné je ručně vytvářet před
prvním použitím, proto nám práci usnadní zmíněná továrnička:
protected function createComponentPostForm()
{
$form = new AppForm();
$form->addText('author', 'Jméno:', 30, 50)
->addRule(Form::FILLED, 'Jméno je povinné.');
$form->addTextArea('text', 'Text:', 50, 8)
->addRule(Form::FILLED, 'Text příspěvku je povinný.');
$form->addSubmit('save', 'Přidat příspěvek');
$form->onSubmit[] = array($this, 'postForm_onSubmit');
return $form;
}
Tato továrnička bude vytvářet formulář s názvem
postForm. Tento název pro nás bude důležitý hlavně při
vykreslování formuláře v šabloně. Formulář má 2 políčka, jedno na
jméno a druhé na text. Obě jsou povinná, nechceme přeci anonymní
příspěvky bez textu. Pod políčky je tlačítko na odeslání, o odeslání
se bude starat metoda postForm_onSubmit() v aktuálním třídě
(tedy ve třídě HomepagePresenter). Aby formulář po odeslání
příspěvek skutečně přidal, musíme si onu metodu nadefinovat:
public function postForm_onSubmit(Form $form)
{
$entry = $form->getValues();
$entry['posted'] = new DateTime();
$entry['ip'] = Environment::getHttpRequest()->remoteAddress;
$this->entries->insert($entry);
$this->flashMessage('Váš příspěvek byl uložen. Děkujeme za Váš čas.');
$this->redirect('this');
}
Vykreslení samotného formuláře na stránce provedeme v šabloně pomocí
makra {control}, kterému jako parametr udáme název komponenty,
v našem případě postForm. Toto makro si od presenteru
vyžádá danou komponentu a ten, pokud již komponenta neexistuje, zavolá
naší továrničku a komponentu vytvoří.
V názvu komponenty je rozlišována velikost písmen a při použití továrniček začíná název komponenty vždy malým písmenem. Pokud dostáváte od aplikace chybu o neexistující komponentě, zkontrolujte právě velikost písmen.
V některých příkladech můžete narazit i na použití
makra {widget}. Pokud Vás zajímá, jaký je mezi těmito dvěma
makry rozdíl, tak vězte, že žádný. {control} je jen
z historického hlediska aliasem pro {widget}.
{block content}
<h1>Kniha návštěv</h1>
{control postForm}
<div class="list">
{* ... *}
</div>
Nyní už by měla kniha fungovat a hosté mohou psát:

Teď už si jistě říkáte: kde je ten slibovaný AJAX? Nebude teď dost práce tam přidat AJAXová volání, AJAXové zpracování… ? Nebude.
AJAX
Ještě než se pustíme do přidělání AJAXu do samotné aplikace, musíme
se postarat o správné přepsání událostí v JavaScriptu, aby vůbec
došlo k jeho volání. Za tímto účelem si vytvoříme malý script ve
složce document_root/js. Nazveme jej třeba ajax.js.
Jeho obsah bude zhruba následující:
/* Volání AJAXu u všech odkazů s třídou ajax */
$("a.ajax").live("click", function (event) {
event.preventDefault();
$.get(this.href);
});
/* AJAXové odeslání formulářů */
$("form.ajax").live("submit", function () {
$(this).ajaxSubmit();
return false;
});
$("form.ajax :submit").live("click", function () {
$(this).ajaxSubmit();
return false;
});
První část scriptu přidá všem odkazům s třídou ajax
událost, která po kliknutí na ně vykoná AJAXový požadavek a zruší
přechod na další stránku. Druhá část, která se týká formulářů, má
obdobný efekt: po odeslání formuláře se data odešlou pomocí AJAXu a
odeslání normální cestou se přeruší. Použití funkce live
zajišťuje, že se událost přidá jak všem současným prvkům, tak i těm,
které do stránky budou přidány – například AJAXem.
Volání funkce live pro událost
submit je možné až od jQuery verze 1.4. Pro nižší verze
použijte plugin Live
Query.
Opět nalinkujeme do stránky v @layout.phtml. A nyní již
hurá na přidání AJAXu!
Snippety
Nejjednodušším způsobem, jak překreslit část stránky v Nette, je
uzavřít ji do snippetu a ten překreslovat. V našem případě budeme mít
snippety tři – formulář, který budeme chtít po úspěšném odeslání
vyprázdnit, seznam příspěvků a flash zprávičky v
@layout.phtml. Současná stabilní verze také vyžaduje použití
zavináčové
magie, takže musíme přidat zavináč před úvodní makro {block
content}. Šablonu default.phtml tedy upravíme takto:
@{block content}
<h1>Kniha návštěv</h1>
{snippet form}
{control postForm}
{/snippet}
{snippet list}
<div class="list">
{if count($entries) > 0}
{foreach $entries as $entry}
<div class="entry">
<div class="author">{$entry->author}</div>
<div class="text">{!$entry->text|escape|nl2br}</div>
<div class="posted">{$entry->posted|date}</div>
</div>
{/foreach}
{else}
<div class="notice">Kniha návštěv zatím neobsahuje žádné příspěvky.</div>
{/if}
</div>
{/snippet}
Ve vývojové verzi Nette 1.0-dev nemusíte zavináče
používat, musíte ale pak používat nové snippety. Ty se od starých
odlišují dvojtečkou místo mezery mezi klíčovým slovem
snippet a názvem snippetu: {snippet:form},
{snippet:list}.
Poslední snippet přijde do šablony @layout.phtml a bude
obalovat vykreslování flash zpráviček – i uživatelům s AJAXem je
jistě budeme chtít zobrazit. Také nesmíme zapomenout na zavináč před
makro {include #content}, jinak by při AJAXových požadavcích
nedocházelo k vkládání (a tím pádem ani k vykonání) bloku a snippety
by nefungovaly.
<body>
{snippet flashes}
{foreach $flashes as $flash}<div class="flash {$flash->type}">{$flash->message}</div>{/foreach}
{/snippet}
@{include #content}
</body>
Změny v presenteru
Změn v samotném presenteru nebude mnoho. Formuláři jen přiřadíme třídu AJAX a mírně poupravíme zpracování formuláře:
protected function createComponentPostForm()
{
$form = new AppForm();
$form->getElementPrototype()->class('ajax');
// ...
}
public function postForm_onSubmit(Form $form)
{
$entry = $form->getValues();
$entry['posted'] = new DateTime();
$entry['ip'] = Environment::getHttpRequest()->remoteAddress;
$this->entries->insert($entry);
$this->flashMessage('Váš příspěvek byl uložen. Děkujeme za Váš čas.');
if (!$this->isAjax())
$this->redirect('this');
else {
$this->invalidateControl('list');
$this->invalidateControl('form');
$form->setValues(array(), TRUE);
}
}
Na konec jsme jen přidali podmínku – v případě AJAXového požadavku
neprovádíme přesměrování, ale zneplatníme dva snippety a voláním
$form->setValues(array(), TRUE); vyprázdníme formulář.
Jediný snippet, který jsme nezneplatnili, byl ten kolem flash zpráviček.
Drobnou funkcí umístěnou do třídy BasePresenter se však
o jejich zneplatnění nemusíme vůbec starat a vše může probíhat
automaticky. Do třídy BasePresenter v souboru
app/presenters/BasePresenter.php tedy můžeme umístit
následující funkci:
public function afterRender()
{
if ($this->isAjax() && $this->hasFlashSession())
$this->invalidateControl('flashes');
}
Ta zajistí, že v případě nastavených flash zpráviček se u AJAXového požadavku snippet automaticky invaliduje a my se o to vůbec nemusíme starat.
A to je vše. Nyní už by se měl formulář odeslat AJAXem a seznam příspěvků by se měl automaticky aktualizovat.
Stránkování
Máme již sice před sebou plně funkční knihu návštěv, která navíc používá AJAX, něco tomu ale stále chybí – stránkování. Po čase by se naše kniha návštěv značně zaplnila a znepřehlednila samými pozitivními komentáři, takže je moudré je rozdělit do stránek.
K tomu si vypůjčíme již hotovou komponentu VisualPaginator. Nette již sice obsahuje třídu Paginator, ta ale obsahuje jen základní logiku potřebnou ke stránkování a neumí vykreslit žádný pro uživatele přívětivý výstup. Komponenta VisualPaginator je jen jakousi obálkou, která se stará o vykreslování zmíněné třídy.
Při používání komponent třetích stran věnujte prosím pozornost její licenci. Některé licence Vám neumožňují použít danou komponentu, pokud nesplňujete určité podmínky. Například komponenty s licencí GNU GPL můžete s projektem distribuovat jen tehdy, kdy i samotný projekt bude distribuován pod licencí GNU GPL. Toto omezení se však týká jen distribuce projektu – pokud projekt nebudete nijak šířit, můžete komponentu použít bez problémů. Toto je vhodné si uvědomit zejména u komerčních projektů, kdy se i dodání webu zákazníkovi považuje za distribuci. VisualPaginator je šířen pod licencí New BSD, která povoluje prakticky jakékoliv použití za předpokladu, že budou v komponentě ponechány copyrighty a prohlášení o zodpovědnosti za škodu.
Vytvoříme si složku app/components a do ní rozbalíme
složku VisualPaginator z distribučního archivu s komponentou.
Stylopis example.css můžeme přesunout do složky
document_root/css a nalinkovat do stránky. Nyní máme vše
připravené a můžeme se pustit do samotné implementace stránkování.
Začneme od modelu. Náš současný model umožňuje jen získání celého
seznamu v databázi. Pokud budeme stránkovat, bude praktičtější, když už
samotný dotaz bude obsahovat klauzule LIMIT a OFFSET,
které nám rozsah výsledků patřičně omezí. Můžeme si tedy upravit
metodu fetchAll() třídy EntriesModel tak, aby toto
omezení zohledňovala. O něco praktičtější však bude použít třídu
DibiDataSource, která je pro tento účel přímo stvořená.
Do třídy BaseModel tedy přidáme novou metodu:
getDataSource, která vrátí novou instanci
DibiDataSource:
/**
* Creates a new DataSource
* @return DibiDataSource
*/
public function getDataSource()
{
return new DibiDataSource($this->name, $this->connection);
}
Třídě DibiDataSource se jako první argument konstruktoru
zadává zdroj, ze kterého se mají data vybírat. To může být buď název
tabulky, jako v našem případě, nebo SQL dotaz. V případě použití SQL
dotazu se použije jako poddotaz.
Použití SQL dotazu se nedoporučuje v případě MySQL
databáze. Ta totiž neumí použít indexy v tabulkách z poddotazu, takže
je poté DibiDataSource silně neefektivní.
Nyní se přesuneme do třídy HomepagePresenter, která bude
hlavním dějištěm našeho stránkování. Nejdříve upravíme metodu
renderDefault() tak, aby prozatím používala novou metodu modelu,
ale zatím nestránkovala:
public function renderDefault()
{
$dataSource = $this->entries->getDataSource();
$dataSource->orderBy('posted', dibi::DESC);
$this->template->entries = $dataSource;
}
Voláním metody orderBy() nad objektem $dataSource
nastavujeme totéž, co jsme dřív předávali jako parametr metodě
fetchAll – sestupné řazení podle sloupce
posted.
Použití DibiDataSource v šabloně bude stejné, jako by šlo
již o hotový výsledek. Při pokusu procházet přes jeho prvky se totiž
automaticky vykoná výsledný dotaz a pro procházení se použije jeho
výsledek. To nám umožňuje libovolně upravovat parametry
DibiDataSource až do jeho použití při vykreslování.
Nyní se pustíme do samotného stránkování. Vytvoříme si továrničku na komponentu VisualPaginator:
protected function createComponentPaginator()
{
$visualPaginator = new VisualPaginator();
$visualPaginator->paginator->itemsPerPage = 10;
return $visualPaginator;
}
Povšimněte si řádku $visualPaginator->paginator->itemsPerPage
= 10;. Jak již bylo dříve zmíněno, slouží třída
VisualPaginator jako obálka nad třídou Paginator.
Právě ta řídí veškerou stránkovací logiku a parametry stránkování
musíme přiřazovat právě jí. Tu třída VisualPaginator
obsahuje v property
paginator. Zmíněný řádek tedy třídě Paginator
říká, že si přejeme na stránce zobrazit 10 záznamů.
Dále musíme zohlednit stránkování při sestavování
DibiDataSource. Je potřeba předat třídě Paginator
informace o celkovém počtu objektů v databázi a objektu
DibiDataSource naopak nastavit pomocí metody
applyLimit() limit a offset. Oba parametry získáme ze třídy
Paginator. Celá metoda renderDefault() bude nyní
vypadat takto:
public function renderDefault()
{
$dataSource = $this->entries->getDataSource();
$dataSource->orderBy('posted', dibi::DESC);
$paginator = $this['paginator']->getPaginator();
$paginator->itemCount = $dataSource->getTotalCount();
$dataSource->applyLimit($paginator->itemsPerPage, $paginator->offset);
$this->template->entries = $dataSource;
}
Nyní už nám zbývá jen naší novou komponentu ve stránce vykreslit.
Opět použijeme makro {control} a umístíme jí do snippetu
list:
{snippet list}
<div class="list">
{if count($entries) > 0}
{control paginator}
{foreach $entries as $entry}
<div class="entry">
<div class="author">{$entry->author}</div>
<div class="text">{!$entry->text|escape|nl2br}</div>
<div class="posted">{$entry->posted|date}</div>
</div>
{/foreach}
{control paginator}
{else}
<div class="notice">Kniha návštěv zatím neobsahuje žádné příspěvky.</div>
{/if}
</div>
{/snippet}
Nyní se už stránkování nejen zobrazí, ale také je plně funkční.

Ale pozor! Při změně stránky se nepoužívá AJAX. Po kliknutí na odkaz vůbec nedojde k AJAXovému volání, navíc by zatím ani nedošlo k překreslení žádného snippetu. Pojďme to tedy napravit…
Pro přidání AJAXového volání po kliknutí na odkaz v máme 2 možnosti:
- přidáme odkazům v šabloně paginatoru třídu
ajax - upravíme
ajax.jstak, aby AJAXová volání přiřadil automaticky i odkazům stránkovače
Zvolíme druhé řešení, protože je méně pracné – stačí jen
přidat selektor .paginator a do ajax.js:
$("a.ajax, .paginator a").live("click", function (event) {
event.preventDefault();
$.get(this.href);
});
Překreslení snippetu je jednoduché, nicméně mírně neelegantní – komponenta VisualPaginator neobsahuje žádný mechanismus, kterým by bylo možné zajistit vyvolání vlastního kódu v případě změny stránky. V podstatě ani není možné takový mechanismus elegantně zajistit – komponenta neví, jakou stránku měl uživatel, od kterého AJAXový požadavek přišel, právě zobrazenou. Vše by se muselo řešit přes dodatečný parametr.
Dovolíme si tedy použít jednodušší řešení – snippet se seznamem příspěvků nebudeme zneplatňovat pouze v případě, že uživatel odeslal formulář, ale při každém požadavku.
Volání invalidate() se nám tedy přesune do metody
renderDefault():
public function renderDefault()
{
// ...
$this->template->entries = $dataSource;
if ($this->isAjax())
$this->invalidateControl('list');
}
Také by bylo vhodné uživateli po odeslání formuláře zobrazit první
stránku s jeho příspěvkem. Přidáme tedy do metody
postForm_onSubmit následující řádek:
$this['paginator']->page = $this['paginator']->paginator->page = 1;
Řádek obsahuje dvě přiřazení – bohužel je nutné současnou
stránku zvlášť nastavit komponentě VisualPaginator a zvlášť třídě
Paginator, kterou komponenta obsahuje.
V tento okamžik by i stránkování mělo fungovat AJAXově a naše návštěvní kniha je zase o kousek lepší.
Závěr
Sestavili jsme jednoduchou knihu návštěv v Nette, která používá AJAX. Trochu paradoxně jsme naprostou většinu času strávili psaním základního kódu, který s AJAXem nesouvisel, a změny při přidávání AJAXu nad celou aplikaci byly minimální.
Nabízí se další rozšíření návštěvní knihy – ochrana proti spamu, moderování příspěvků… Některá rozšíření mohou postupně přibýt do tohoto tutoriálu, záleží na Vašem zájmu.
Celou aplikaci si můžete vyzkoušet i stáhnout.
Komentáře 
Jan Tvrdík | 28. 2. 2010, 23:48 | comment
Přimlouvám se za pokračování – stránkování, Captcha, …
be-online | 1. 3. 2010, 0:28 | comment
Naprosto vynikající tutoriál … Díky za něj !
Panda | 2. 3. 2010, 13:51 | comment
První přání bylo vyslyšeno – již stránkujeme.
Acnnair | 2. 3. 2010, 15:19 | comment
Paráda, super tutoriál. Len v 1.0 dev mi nefungujú úplne snippety v tvare {snippet:nazov}. Zoznam sa mi prekreslí, ale paginator sa už nevykreslí. Nevie niekto v čom to môže byť?
Panda | 2. 3. 2010, 16:13 | comment
Stránkování jsem dopisoval dneska a přiznám se, že s novými snippety jsem to zkoušel až teď. Nové snippety ještě nejsou v Nette hotové, bude to nějaký bug. Zkusím se v tom trochu pošťourat a případně dodat opravu. Pokud bych nenašel žádný rozumný způsob, tak přidám ke stránkování upozornění, že zatím nebude fungovat s novými snippety.
Tharos | 2. 3. 2010, 16:27 | comment
Vynikající tutoriál, díky!
Acnnair | 2. 3. 2010, 17:25 | comment
Panda: Ďakujem za vyjadrenie. Zatiaľ si vystačím aj so starými, len som teda nevedel, že či mi tam ešte niečo chýba alebo ako. :-)
Panda | 2. 3. 2010, 17:29 | comment
Acnnair: zkus se podívat na toto: Komponenty ve snippetu se nerenderují při AJAXu. Dej pak vědět, jestli úprava pomohla.
jfojtl | 3. 3. 2010, 12:59 | question
Nazdárek, díky za tutoriál.
Chtěl jsem se zeptat, jak by se příklad změnil, kdybych chtěl s novým příspěvkem nadále pracovat po uložení do DB. Třeba mu přidat podobný efekt jako na FB, když změníte svůj status, pomocí jQuery, například metodou slideDown(). Pokud si teď pomocí snippetu nahraju celý stream nově, tak to asi moc dobře nepůjde :(
Panda | 3. 3. 2010, 13:58 | comment
Příklad by se nezměnil až tak výrazně. Připravil jsem příklad: ajax-guestbook-slideDown.zip. Demo: http://jan.smitka.org/…deDown/demo/
Postupoval jsem asi takto:
- V případě, že byl odeslán formulář, neinvaliduji snippet
list. - Aby nedošlo k normálnímu zpracování, tak třídu formuláře změním
z
ajaxna nějakou jinou, např.postForm. - Po odeslání formuláře v případě AJAXového požadavku přiřadím do
proměnné
$newEntryv šabloně objekt s novým příspěvkem a invaliduji snippetnewEntry, který vytvořím později. - V šabloně vytvořím nový blok
entry, který bude umístěn mimo hlavní blokcontent. Do něj přesunu šablonu příspěvku (tzn.<div class="entry"> ... </div>) a do původního cykluforeachvložím místo ní{include #entry, entry => $entry}. Před počáteční ({block entry}) i koncové ({/block}) makro bloku musím dát zavináč. - Kamkoliv do bloku
contentvložím podmínku, zda byla nastavená proměnná$newEntry, a do ní snippetnewEntry. V tomto snippetu budu renderovat nový příspěvek:{include #entry, entry => $newEntry}. - Do
ajax.jspřidám handler, který zpracuje odeslání formuláře s třídoupostForm. Ten z vrácených dat vytáhne snippetnewEntrya jeho obsah připojí před seznam současných příspěvků. Daný snippet na stránce neexistuje (je v podmínce), takže to bude jediné jeho zpracování. Handler může vypadat takto:
$('.postForm').live('submit', function () {
$(this).ajaxSubmit(function (payload) {
// Zavoláme původní handler, který překreslí ostatní snippety
jQuery.nette.success(payload);
if (payload.snippets) {
if (payload.snippets['snippet--newEntry']) {
var newEntry = $(payload.snippets['snippet--newEntry']);
newEntry.hide();
$('#snippet--list .entry:first').before(newEntry);
newEntry.slideDown(500);
// Sem by se ještě měla přidat kontrola, zda není
// zobrazena hláška o prázdné knize hostů,
// ale to je asi už jen detail.
}
}
});
return false;
});
Myslíte, že bych tuto variantu také měl nějak rozvést v tutoriálu?
Oggy | 3. 3. 2010, 15:46 | comment
Musím se přidat k pochvalám.. skvěle napsaný, okomentovaný tutoriál.
Oggy | 3. 3. 2010, 15:53 | comment
To: Panda myslím, že pokud máš chuť, tak čím více informací, tím lépe. Také jsem si s tvým příkladem hrál.. chtěl jsem přidat možnost odpovědět na daný příspěvek.. a u příspěvku uvádět, na co reaguje sám a které příspěvky reagují na něj.. jediný problém byl v tom, že jsem na něj chtěl odkazovat – pomocí fragmentu #entryid-xx, ale je potřeba při použití stránkování zjistit na které stránce příspěvek na který odkazuji je..
jfojtl | 4. 3. 2010, 0:10 | comment
Safra, tak to jsem nečekal :) Děkuju moc, určitě pomohlo. Mě osobně se tato varianta líbí o něco víc, než co je v tutorialu, určitě bych to minimálně zmínil.
theo | 16. 3. 2010, 11:38 | question
Mno velice pěkný tutorial, ze kterého jsem se dozvěděl jak na to, nicméně je tam ještě jeden drobný problém spočívající v tom, že flashMessage zůstane viset i při dalším stránkování. Jak by se řešil tento problém? David psal kdesi na fóru cosi o tom, že flashMessage by se měl dát nastavit čas platnosti (dokonce, že tam snad i nějaký výchozí – 3 vteřiny, jestli mě paměť neklame – je nastavený).
Panda | 16. 3. 2010, 19:02 | comment
V platnosti problém není, ta slouží v podstatě jen pro interní účely. Problém je v tom, že po přechodu na jinou stránku se Ti nepřekreslí snippet s flash zprávičkou. Řešení mám 2:
- odstranění
$this->hasFlashSession()z podmínky pro invalidaci snippetu, což je méně elegantní (snippet se překreslí vždy) - automatické skrývání flash zpráviček s pluginem livequery po určitém časovém intervalu:
$("div.flash").livequery(function () {
$(this).delay(10000).animate({"opacity": 0}, 2000).slideUp();
});
// Řešení inspirováno skrýváním flash zpráviček z ukázkové aplikace pro DataGrid od Romana Sklenáře:
// http://github.com/romansklenar/nette-datagrid/blob/master/document_root/js/datagrid.js
cmman | 5. 4. 2010, 15:04 | bug
Děkuji za tutorial a dovolím si drobnou poznámku. Nemá náhodou být
public function postForm_onSubmit(Form $form) {/../ $this->getEntries()->insert($entry); /../ }
místo
public function postForm_onSubmit(Form $form) {/../ $this->getEntriesModel()->insert($entry); /../}
? Alespoň takto upravené mi to šlape ;)
Jan Tvrdík | 5. 4. 2010, 17:40 | comment
Díky za upozornění, opraveno.




Jan Tvrdík | 28. 2. 2010, 22:53 | comment
Špičkový tutoriál. Díky za něj, Pando.