contributte / planette-site

💀 [DISCONTINUED] All the roads go through the Planette
https://planette.vercel.app
MIT License
9 stars 3 forks source link

2015-04-24: navstevni-kniha-vyuzivajici-ajax #38

Open paveljanda opened 7 years ago

paveljanda commented 7 years ago

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

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

Tento tutoriál je psán pro Nette 0.9. V současné verzi Nette 2.0 se konfigurace provádí trochu jinak, viz Konfigurace Nette Frameworku.

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

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:

  1. vytvořit si jednu instanci modelu v metodě startup presetneru a tu používat
  2. vytvořit si novou instanci modelu při každém použití v presenteru
  3. 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í:

screen-1.png

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:

screen-2.png

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

Tento tutoriál je psán pro Nette 0.9. V současné verzi Nette 2.0 už se zavináče nepíší, viz Historie ajaxu v Nette, jinak se ale snippety používají velmi podobně.

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}

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

screen-3.png

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:

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.

paveljanda commented 7 years ago

author: 1721 ()