Latte jako od baristky

Latte je šablonovací (templating) systém z českého frameworku Nette (dá se ale použít i samostatně). Jeho nejsilnější vlastností je možnost rozložit celou stránku na sadu menších šablon, ze kterých se pak zpětně stránka poskládá.

Důvodem pro rozdělení stránky na menší části je to, že danou část můžete použít na více místech a změnou jedné šablony pak upravíte několik stránek najednou. Ale i když určitou část nepotřebujete znovu-použít, můžete ji oddělit čistě pro přehlednost.

Způsobů, jak stránku rozdělit na šablony, layouty, komponenty nebo formuláře, je celá řada a vybrat ten správný může být klíčové pro budoucí udržitelnost a rozšiřitelnost vašich stránek.

Poznámka: Vše dále uvedené platí pro Nette 2.4. Ostatní verze se mohou lišit

Základní Latte šablona

Šablony se ukládají do souborů *.latte a obsahují HTML kód. Navíc ale mohou obsahovat složené závorky (což je obdoba PHP tagu), do kterých lze uvádět makra nebo přímo PHP kód (to zde ale nebudu probírat, podívejte se do dokumentace).

Celý šablonovací proces začíná v souboru, jež lze najít podle jmen aktuálního presenteru a akce. Pokud například uživatel navštíví stránku http://server.cz/eshop/notebooky, Latte najde soubor /templates/eshop/notebooky.latte. Výchozí (defaultní) šablona se pak jmenuje default.latte a zobrazí se např. když uživatel navštíví http://server.cz/eshop/ nebo http://server.cz/ (podoba URL záleží na vašem routování).

Pokud máte jednoduchý web, můžete celou stránku napsat do tohoto souboru šablony. Ve většině případů si s tím ale nevystačíte a budete muset stránku rozdělit.

Doporučené použití: Jednoduché stránky, které nemají nic společného s jinými stránkami webu.

Použití jiné než výchozí šablony

V Nette presenteru můžete změnit jméno použité šablony metodou setFile(), např. $this->template->setFile(__DIR__ . '../templates/eshop/list.latte'). Pozor na to, že metoda nemá žádnou výchozí složku, ve které šablony hledá, a tak musíte uvést absolutní cestu (nebo jako zde relativní od umístění daného presenteru). Např.:

public function handleNotebooky() {
    $nb = $this->getNotebooky();
    $this->template->list = $nb;
    $this->template->setFile(
      __DIR__.'/../templates/ehop/list.latte');
      //'list.latte' je obecná šablona
      //pro výpis listu výrobků z $list
}

Doporučené použití: Sdílení šablony pro několik akcí, které mají jiné ošetření akcí nebo signálů ale nakonec zobrazují tutéž stránku (šablonu) nebo stránky s jinými daty ale stejným vzhledem (např. stránky kategorií v eshopu, kde je jiné zboží, ale všechny kategorie vypadají stejně).

Základní layout

Abyste do každého layoutu nemuseli psát HTML hlavičku, i když je stejná, můžete použít tzv. layout (česky „rozložení“, ale používá se anglický název pro jednoznačnost).

Layout může definovat základní a společné části pro všechny stránky:

<html>
    <head>
        <title n:innerblock="title"></title>
    </head>
    <body>
        {include content}
    </body>
</html>

Z tohohle pohledu je layout jen další šablona a proto se také ukládá do souboru *.latte. Liší se ale způsob jejího použití a proto se layouty ukládají do souborů, jejichž jméno začíná zavináčem ‚@‚, např. ‚@eshop.latte‚.

Layout můžete do stránky vložit přes makro {layout '@jmeno-layoutu.latte'}, přičemž pokud toto makro neuvedete, Latte zkusí najít soubor ‚@layout.latte‚ a použít ho. Při hledání layoutu (ať už definovaného nebo výchozího) se vždy začíná ve složce šablony (např. /templates/eshop/) a když neexistuje, zkusí se nadřazená složka (tedy /templates/).

Je jedno, kam do šablony makro uvedete, protože layout se bude zpracovávat až poté, co se zpracuje celá šablona.

Doporučené použití: Složitější web s více stránkami, které musejí vycházet ze stejného HTML základu (hlavička, CSS a JS pro použité frameworky, atd.)

Přepis bloků v layoutu

Vztah šablony a layoutu definují tzv. bloky. V HTML kódu výše jste si mohli všimnout dvou netypických zápisů: {include content} a n:innerblock. To jsou právě místa, která šablonovací systém nahradí částmi definovanými v šabloně. Pokud máme výše uvedený layout, může pak šablona notebooky.latte vypadat takto:

{layout '@layout.latte'}
{block title}Nabídka notebooků{/block}
{block content}
        Zde bude výpis notebooků (BODY stránky)
{/block}

Šablona již nemusí definovat HTML strukturu stránky (HEAD a BODY), protože o to se stará layout. Šablona jen nadefinuje jednotlivé bloky, které se později vloží do layoutu.

Tam, kde použijete {block jmeno-bloku} nebo <​div n:block="jmeno-bloku"> se nahradí celý blok (tedy i uvedený DIV), pokud naopak použijete n:innerblock, nahradí se jen obsah tagu, což je užitečné, pokud chcete vkládaný blok obalit kontejnerem (jako v příkladu výše, kde se zachovává tag TITLE a šablona pak už jen nadefinuje vlastní text titulku).

Další výhoda bloků je v tom, že je můžete vkládat opakovaně. Layout např. může využít nadefinovaného titulku a použít ho jak pro HTML titulek tak i pro hlavičkový titulek (H1):

<html>
    <head>
        <title n:innerblock="title"></title>
    </head>
    <body>
         <​h1>{include title}</h1>
        {include content}
    </body>
</html>

Ověření existence bloku

Ve složitějším projektu můžete narazit na to, že budete chtít v layoutu (nebo jinde, viz dále) includovat blok, ale nebudete mít jistotu, zda existuje. K tomu slouží makro {ifset}, které (mimo jiné) může ověřit existenci bloku:

    ...
        {ifset content}
            {include content}
        {/ifset}
    ...

Pokud blok content nebyl v šabloně definován, v layoutu se nevytiskne (a nevypíše se chyba „Undefined block„).

Povinné bloky v šabloně

Důležité je si uvědomit, že v okamžiku, kdy se šablona vkládá do layoutu, musí obsahovat bloky. Vše, co se nachází mimo blok, bude šablonovacím systémem zahozeno. Využít toho lze např. pro komentář:

Stránka s notebooky pro eshop
(Toto se nikde nezobrazí)

{layout '@layout.latte'}
{block title}Nabídka notebooků{/block}
{block content}
    Zde bude výpis notebooků (BODY stránky)
{/block}

Mnohem užitečnější je ale využít toho pro vytvoření šablony, která může fungovat samostatně i jako součást layoutu:

<html>
    <head>
        <title
         n:innerblock="title">Notebooky</title>
    </head>
    <body n:innerblock="content">
        Zde bude výpis notebooků (BODY stránky)
    </body>
</html>

Když takovouhle šablonu zobrazíte bez layoutu, vygeneruje celou (jednoduchou) HTML stránku. Pokud ji ale vložíte do layoutu, zachovají se jen bloky title a content, které nahradí stejnojmenné bloky v layoutu a zbytek se zahodí.

Typ bloku

Může se vám stát, že vám vyskočí chyba „Warning: Overridden block xxx with content type YYY by incompatible type ZZZ.“. Bloky jsou totiž content-aware a během přepisování, vkládání a dědění musí mít stejný typ. Pokud tedy v layoutu nadefinujete blok např. uvnitř skriptu, bude mít typ SCRIPT. Když pak v šabloně nadefinujete ten samý blok, bude mít typ HTML a vzniká nekompatibilita.

Obejít to můžete díky tomu, že v šabloně se kód mimo bloky zahodí a tak můžete blok obalit potřebným kódem aby získal požadovaný typ.

Příklad pro skript:

{* layout *}
<script>{block myScript}
    alert('Layout');
{/block}</script>
{* template *}

{* Tohle způsobí nekompatibilitu *}
{block myScript}
    alert('Template');
{/block}

{* Tohle je v pořádku *}
<script>{block myScript}
    alert('Template');
{/block}</script>

Ještě jeden příklad pro HTML atributy:

{* layout *}
<body {block body-attr}{/block}>
{* template *}

{* Tohle způsobí nekompatibilitu *}
{block body-attr}class="template"{/block}

{* Tohle je v pořádku *}
<div {block body-attr}class="template"{/block}>
</div>

Dědění šablon

Rozdělení na výchozí layout a šablonu je pěkné, ale u složitějších webů si s tím moc nevystačíte. V našem příkladu s eshopem budeme potřebovat, aby na stránce http://server.cz/ byla uvítací stránka firmy (např. se seznamem kamenných provozoven), zatímco na http://server.cz/eshop byla stránka eshopu s nákupním košíkem a seznamem kategorií.

Toho dosáhneme díky dědění layoutů, kdy jeden layout může makrem {layout} vložit jiný layout a „obalit se jím“.

Šablona s notebooky by pak použila:

{layout '@eshop.latte'}

{block title}Nabídka notebooků{/block}
{block eshop}
    Zde bude výpis notebooků (BODY stránky)
{/block}

Latte pak použije layout /templates/eshop/@eshop.latte nebo /templates/@eshop.latte, který bude vypadat následovně:

{layout '@layout.latte'}

{block content}
    {block basket}
        Zde bude nákupní košík
    {/block}

    {include eshop} {* blok ze šablony *}
{/block}

Layout eshopu tedy vykreslí nákupní košík (jak to udělat si ukážeme dále) a pak vykreslí obsah ze šablony. Následně se sám obalí layoutem @layout.latte, který zůstává stejný jako výše uvedený a definuje layout pro všechny stránky.

Všimněte si, že šablona notebooky.latte místo bloku content definuje blok eshop, který se pak použije v šabloně @eshop.latte. Blok content je nadefinován až v @eshop.latte a bude se vkládat do @layout.latte.

Dobré je používat blok content pro to, co se vloží do základní šablony ‚@layout.latte‚ a pro ostatní layouty použít blok se stejným jménem jako daný layout. Mezi základními šablonamy a layouty se totiž jména bloků sdílí a přepisují, což je vidět na bloku title, který se z notebooky.latte přenese rovnou do @layout.latte.

Pozor na to, že pokud budete mít layout @eshop.latte umístěn v podsložce (/templates/eshop/) ale hlavní layout v základní složce (/templates/), musíte tomu přizpůsobit cestu, protože při dědění layoutu již nedochází k automatickému dohledání cesty (jak je tomu při vkládání ze šablony). Layout by se tedy vkládal makrem {layout '../@layout.latte'}.

Poznámka: Nevím, zda se jedná o chybu nebo vlastnost, ale pokud máte dvě šablony v hlavní složce (/templates/) a v jedné uvedete cestu k druhé s počátečním ‚../‚, šablona se stejně najde.

Doporučení použití: Web, který obsahuje stránky z různých oblastí použití, které mají společné části odlišné od jiných oblastí (např. eshop, články, galerie, atd.)

Layout bez obsahu

Výše jsem uváděl, že layout by měl includovat hlavní blok s obsahem ze šablony nebo předcházejícího layoutu. To ale není pravda. Pokud layout nepotřebuje obalovat obsah nadefinovaný v šabloně, může šablona přímo vyplnit obsah (tedy content) v hlavním layoutu. Layout může jen přidat nebo změnit některé bloky:

{* notebooky.latte *}
{layout '@eshop.latte'}
{block title}Nabídka notebooků{/block}

{* šablona mění přímo blok z @layout.latte *}
{block content}
    Zde bude výpis notebooků (BODY stránky)
{/block}
{* @eshop.latte *}
{layout '@layout.latte'} 

{* nic nedělej, jen předej bloky dál *}

Pořadí přepisu bloků

Důležité je si nyní uvědomit, v jakém pořadí se budou přepisovat bloky definované v šabloně a obou layoutech:

  • Nejprve se zpracuje notebooky.latte a vytahají se z něj bloky definované přes {block} (ostatní kód se zahodí).
  • Následně se zpracuje @eshop.latte, vloží se do něj bloky z noteboky.latte tam, kde je {include} a bloky v @eshop.latte definované přes {block} se nahradí stejnojmennými bloky z notebooky.latte.
  • Nakonec se zpracuje @layout.latte a vloží se bloky z @eshop.latte a notebooky.latte tam, kde je {include}, přičemž pokud @eshop.latte a notebooky.latte obsahují stejnojmenný blok, použije se ten z notebooky.latte (protože v předchozím kroku se ten z @eshop.latte již nahradil tím z notebooky.latte). Tam, kde @layout.latte obsahuje {block}, nahradí se stejnojmenným blokem z @eshop.latte nebo nootebooky.latte (opět se stejnými pravidla jako {include}).

Praktický příklad na přepis bloků může být hlavička, patička a menu stránky:

/* @layout.latte */
<​menu n:innerBlock>Menu serveru</menu>
<​h1 n:innerBlock="header">Můj server</h1>
<​footer n:innerBlock="footer">© Moje</footer>
{block debug}Proměnné: {dump}{block}
/* @eshop.latte */
{block menu}Menu eshopu{/block}
{block header}Eshop{/block}
{* patička se nemění *}
/* notebooky.latte */
{* nemá vlastní menu *}
{block header}Notebooky{/block}
{block footer}Na vše 2 roky záruka{/block}

Výsledná stránka s notebooky bude obsahovat menu z @eshop.latte (protože to přepsalo menu serveru), hlavičku z notebooky.latte (protože ta vyhrála nad hlavičkou z @eshop.latte a @layout.latte) a patičku z @eshop.latte (která přepsala tu z @layout.latte). Výpis proměnných zůstane jak je, protože @eshop.latte ani notebooky.latte tento blok nedefinují. Vše díky tomu, že bloky se přepisují „zdola nahoru„.

Výhoda přepisu bloků je v tom, že pokud chci na nějaké stránce (nebo stránkách) změnit jednu konkrétní část, nadefinuji v ní stejnojmenný blok, který pak nahradí daný blok v nadřazeném layoutu. A pokud blok nenadefinuji, zůstane původní obsah.

Doporučené použití: Tam, kde chcete pro určitou stránku nebo stránky změnit část nadřazeného layoutu (např. patičku) nebo doplnit něco navíc (třeba menu).

Obrácené vkládání bloků

Pokud chcete zachovat oba (nebo všechny 3) obsahy ze stejnojmenných bloků, musíte v bloku použít makro {include parent}. Opět je ale potřeba si uvědomit, co se bude kam vkládat:

  • pokud je v souboru notebooky.latte použito {include parent} vloží se stejnojmenný blok z @eshop.latte nebo (pokud neexistuje) z layout.latte.
  • pokud je v @eshop.latte použito {include parent}, vloží se stejnojmenný blok z @layout.latte.
  • pokud notebooky.latte vkládá blok z @eshop.latte, který ale také obsahuje {include parent}, vloží se do notebooky.latte bloky z @eshop.latte i @layout.latte (v takovém pořadí, jaké definuje použití {include parent}).

Praktický příklad na {include parent} je blok title, který definuje titulek stránky (v našem příkladu):

/* @layout.latte */
<title n:innerblock="title">Můj server</title>
/* @eshop.latte */
{block title}{include parent} má Eshop{/block}
/* notebooky.latte */
{block title}Notebooky - {include parent}{/block}

Titulek stránky bude „Notebooky – Můj server má Eshop„, protože bloky se budou vkládat „shora dolu„. Výhoda tohoto vkládání je snad zřejmá – pokud chci změnit jméno serveru, přepíšu ho jen v @layout.latte a všechny stránky (tedy šablony) automaticky použijí nové jméno.

V případě, že by @eshop.latte nedefinoval vlastní blok title, vložení by stále fungovalo a titulek by byl „Notebooky – Můj server„. Pokud by naopak definoval blok title bez použití {include parent}, jméno serveru by se na stránce s notebooky vůbec nezobrazilo.

Doporučené použití: Tam, kde v určité šabloně potřebuji vypsat část, která je již nadefinována v nadřazené šabloně a je potřeba, aby byla na všech stránkách stejná.

Skrytý sdílený blok

Pokud potřebujete v šabloně použít část, kterou chcete nadefinovat v layoutu, ale nechcete, aby se v layoutu zobrazila, pokud se v šabloně nepoužije, můžete místo {block} použít {define}:

/* @eshop.latte */
{define vyzvednuti}v {$nextWorkDay}{/define}
/* notebooky.latte */
{block vyzvednuti}Toto zboží si budete moci vyzvednout {include parent}{/block}

Doporučené použití: Sdílení částí kódů, které mají přímý vztah s daným layoutem, v různých šablonách. Pro obecnější části kódu je lepší použít samostatnou šablonou (viz dále).

Samostatná šablona

Pokud máte část stránky, která je na zbytku kódu nezávislá, lze ji použít na několika stránkách nebo se ve stránce opakuje, můžete ji oddělit do samostatné šablony a pak vložit do hlavní šablony nebo layoutu. Jako příklad použijeme nákupní košík uvedený výše.

/* basket.latte */
{block basket}
<​div>Nákupní košík</div>
<​ul>
   <li n:foreach="$nakup as $i">
       {$i->name}
   </li>
<​/ul>
{/block}

Samostatná šablona stále může přistupovat k proměnným z template, ale nijak neovlivní nadřazené šablony ani layouty. Pokud tedy bude definovat vlastní bloky, nepřepíší bloky z ostatních šablon a layoutů ani nepůjdou vkládat makrem {include}.

Do layoutu @eshop.latte můžete nákupní košík vložit makrem {include} a místo jména bloku uvést jméno souboru:

/* @eshop.latte */
{layout '@layout.latte'}

{include 'basket.latte'}

{include eshop}

Pokud byste použili jen {include basket} (ať už před, místo nebo po {include 'basket.latte'}), dostali byste chybu, že blok neexistuje (i když v basket.latte je nadefinovaný). Jak vkládat bloky si ukážene dále.

Při vkládání opět musíte správně uvést relativní cestu ke složce s výchozí šablonou. Pokud je v jiné složce, musíte použít např. {include '../shared/basket.latte'}.

Doporučení použití: Části stránky, které chcete mít oddělené od zbytku kódu stránky pro snažší úpravu či změnu. Šikovné je pro A/B testy, kde můžete změnou vnořené šablony zobrazit jiný kód (bez zásahu do hlavní šablony).

Proměnné pro samostatnou šablonu

Proměnnou $nakup můžete v Nette vložit do šablony v metodě beforeRender(), která se volá pro všechny akce (výchozí šablony) a tudíž zajistí, že samostatná šablona dostane potřebná data:

public function beforeRender() {
    parent::beforeRender(); /*nezapomenout!!!*/

    $this->template->nakup = $this->getNakup();
}

Pokud lze samostatnou šablonu použít ve více presenterech, můžete beforeRender() nadefinovat v BasePresenter, od kterého by všechny ostatní presentery měli být odděděny.

Pokud naopak používáte Latte bez Nette frameworku, záleží, jak template vytváříte. Pokud používáte celý Latte engine, vkládáte proměnné takto:

$latte = new Latte\Engine;
$params = ['nakup' => Nakup::load()];
$latte->render('notebooky.latte', $params);

Pokud používáte jen samotný template, můžete do něj vložit proměnné přímo – stejně jako v presenteru:

use Nette\Templating\FileTemplate;

$template = new FileTemplate('notebooky.latte');
$template->nakup = Nakup::load();
$template->render();

Doporučené použití: Část stránky, která vypisuje nějaká data (načítaná např. z DB), ale opakuje se na více stránkách, takže je potřeba kód sdílet.

Data pro samostatnou šablonu

Do samostatné šablony můžete také předávat data ze šablony, která ji používá. Pro příklad si udělejme šablonu pro vykreslení jedné položky nákupního košíku:

/* basketItem.latte */

<div class="basketItem">
    <span class="name">{$item->name}</span>
    <span class="price">{$item->price}</span>
</div>

Šablonu položky pak použijeme v šabloně nákupního košíku a předáme jí danou $item (která může být např. instancí třídy ShopItem), kterou pak vykreslí:

/* basket.latte */
{block basket}
<​div>Nákupní košík</div>
<​ul>
    <li n:foreach="$nakup as $i">
        {include 'basketItem.latte'
            item => $i
        }
    </li>
<​/ul>
{/block}

Všimněte si, jak se vkládá proměnná do samostatné šablony – určíte její jméno (bez dolaru ‚$‚ a uvozovek) a přes šipku ‚=>‚ (t.j. PHP symbol pro hodnotu pole) určíme hodnotu.

Poznámka: proměnné předané do šablony by mohly být přímo nadefinovány jako PHP pole, ale Latte nám usnadňuje práci s tím, že nemusíme psát array() nebo [ ] (podle verze PHP) ani uvozovky pro klíče pole.

Doporučené použití: Opakující se výpis dat (ať už na jedné stránce nebo na více stránkách), která je potřeba nějak obalit. Opět vhodné pro A/B test, kde můžete změnou jedné šablony vypsat data jiným způsobem.

Vkládání bloků ze šablony

Pokud chcete mezi několika stránkami sdílet části, které je ale potřeba vložit do různých částí šablony nebo layoutu (pomocí {block}), nemůžete použít {include} pro danou šablonu, protože jak už jsme si ukázali, použitím {include 'basket.latte'} nedojde k přenosu bloků.

Například, pokud chceme na každé stránce eshopu uvést, že máme záruku 2 roky a kde a jak ji uplatnit, můžeme kód oddělit do samostatné šablony. Jelikož se ale jedná o dvě odlišné informace (záruka a servis), budeme potřebovat použít bloky:

/* warranty.latte */
{block zaruka}
  Na toto zboží platí 2 roky záruky.
{/block}
{block servis}
  Zboží reklamujte na adrese: ...
{/block}

V šabloně notebooky.latte pak můžete záruku vložit pomocí makra {import} a následně vložit bloky tak, jak bychom to dělali v layoutu:

/* notebooky.latte */
{import 'warranty.latte'}

{block eshop}

<div n:foreach="$list as $item">
        {$item->name}
        <span>{include zaruka}</span>
    </div>

    {include servis}
{/block}

Díky importu se ke každému notebooku dopíše informace, že má 2 letou záruku a nakonec seznamu se vypíše adresa servisu. Pozor na to, že při použití {import} musíte následně používat {include}, protože bloky definované přes {block} se bloky z importované šablony nepřepíší!

Jiný příklad z praxe je ten, kdy v šabloně definujete HTML kód a k němu příslušející javascript, ale JS chcete vypsat odděleně až na konci stránky (např. až poté, co se načte jQuery).

Opět platí, že kód uvedený v importované šabloně mimo bloky se zahodí. Stejně tak se zahodí i bloky, které nepoužijete (nevložíte).

Můžete klidně importovat i šablonu, která má další kód než jen ten, který chcete využít. Např. byste mohli do stránky notebooky.latte naimportovat šablonu servisy.latte a pak použit jen blok servispronotebooky.

Doporučené použití: Tam, kde část stránky (uložená s samostatné šabloně) potřebuje nadefinovat dvě a více částí, které je potřeba zobrazit na různých místech. Také lze použít k tomu, abyste na jedné stránce zobrazili část jiné, nesouvisející stránky.

Proměnné pro vložené bloky

Proměnné není možné předat makrem {import}, ale lze je vložit do makra {include}. Můžete tak mít šablonu pro záruku a každý výrobek bude mít vlastní délku záruky:

/* warranty.latte */
{block zaruka}
  Na toto zboží platí {$delka} roky záruky.
{/block}
/* notebooky.latte */
{import 'warranty.latte'}

{block eshop}

<div n:foreach="$list as $item">
        {$item->name}
        <span>{include zaruka
            delka => $item->zaruka}
        </span>
    </div>

{/block}

Druhou možností vložení proměnných je samozřejmě přes template v presenteru (viz příklad s $nakup výše).

Doporučené použití: Tam, kde chcete vypsat opakující se kód, který se ale skládá z více částí, které je potřeba vypsat do různých částí stránky.

Použití komponenty

Pokud chcete do stránka vkládat nějaký složitější kód, který se nedá jednoduše vložit přes šablonu, můžete použít komponentu. Každá komponenta má vlastní šablonu, ale kromě toho může mít další PHP kód, který může použít např. k tomu, aby rozhodla, jakou šablonu použít nebo co do šablony vypsat.

Pokud bychom například nákupní košík udělali jako komponentu místo šablony, nebylo by již potřeba načítat proměnnou $nakup v presenteru, ale komponenta by si to mohla zajistit sama. Nevýhoda Nette komponent je v tom, že jejich vytvoření není jednoduché a potřebujete k tomu pomocné metody (v Nette zvané Factorytovárny).

/* Basket.php */
class Basket
    extends \Nette\Application\UI\Control {

    protected $tpl =
         __DIR__ . '/basket.latte';

    public function render() {
        $t = $this->template;
        $t->setFile($this->tpl);
        $t->nakup = $this->getNakup();
        $t->render();
    }

    protected function getNakup() { ... }
}

Nákupní košík má vlastní šablonu a je pěkně zapuzdřený (tzn. metodu pro načítání dat má v sobě). Použití ale není tak jednoduché, nejprve musíte v presenteru vytvořit tovární metodu:

/* EshopPresenter.php */

    public function createComponentBasket() {
        return new Basket();
    }

A následně můžete v šabloně použít makro {control} pro vložení komponenty vytvořené v továrně:

/* @eshop.latte */
{layout '@layout.latte'}

{block content}
    {control basket} {* komponenta košíku *}
    {include eshop}  {* blok ze šablony *}
{/block}

Doporučené použití: Tam, kde vložení samostatné šablony nestačí a je potřeba další PHP kód.

Komponenta v komponentě

I komponenta může používat jiné komponenty, jen musíte metodu createComponent*() vytvořit v kódu samotné komponenty.

Například pokud bychom vytvořili i komponentu BasketItem (což jsme výše používali jako samostatnou šablonu), komponenta Basket by ji vytvářela následovně:

/* Basket.php */
class Basket
    extends \Nette\Application\UI\Control {

    public function render() { ... }
    protected function getNakup() { ... }

    public function createComponentItem() {
        return new BasketItem();
    }
}

A použití:

/* basket.latte */
{block basket}
    {foreach $nakup as $i}
        {control item $i}
    {/foreach}
{/block}

Všimněte si, že i když se třída komponenty jmenuje BasketItem, můžeme ji v šabloně volat jen jako {control item} díky tomu, že jsme metodu nazvali createComponentItem() – jméno továrny není totiž přímo svázáno s třídou a lze ji přejmenovat. Zde je například jasné, že v nákupním košíku nebudeme použít komponentu MenuItem, takže není potřeba uvádět celé jméno.

Komponenta z více částí

Stejně, jako jsme výše importovali šablonu s několika bloky, může i komponenta vykreslit několik nesouvisejících částí. Pro příklad budeme chtít k nákupnímu košíku přidat Javascript, který přidá nějaké animace:

/* Basket.php */
class Basket
    extends \Nette\Application\UI\Control {

    protected $tpl =
         __DIR__ . '/basket.latte';
    protected $js =
         __DIR__ . '/basket.js';

    public function render() {
        $t = $this->template;
        $t->setFile($this->tpl);
        $t->nakup = $this->getNakup();
        $t->render();
    }

    public function renderJs() {
        $js = file_get_contents($this->js);
        return $js;
    }

    protected function getNakup() { ... }
}

Vytvoření komponenty přes createComponentBasket() zůstává stejné, ale liší se použití makra {control}, kde potřebujete říci, kterou metodu render*() chcete zavolat:

/* @eshop.latte */
{layout '@layout.latte'}

{block content}
    {control basket} {* Basket::render() *}
    {include eshop}  {* blok ze šablony *}
    <​script>
        {* volání Basket:renderJs *}
     {control basket:js}
    <​/​script>
{/block}

Další možností použití různých renderovacích metod může být to, že potřebujeme použít jiný vzhled (jiné šablony) na různých stránkách (např. na platební stránce se zobrazí rozbalený, ale v eshopu bude sbalený a rozbalí se až po kliknutí).

Doporučení použití: Komponenta, která renderuje několik odlišných částí (např. HTML a JS) nebo se na každé stránce renderuje jinak.

Data pro komponentu

Pokud potřebujete komponentě předat nějaká data (která si komponenta nemůže obstarat sama), máte k tomu dvě možnosti.

První možností je předání dat do konstruktoru z továrny:

/* Basket.php */
use \Nette\Database\Connection as Db;
class Basket
    extends \Nette\Application\UI\Control {

    protected $db = null;

    public function construct(Db $db) {
         $this->db = $db;
   }
    protected function getNakup() {
        $this->db->query(...);
    }
}

Data pak předáme v továrně:

/* EshopPresenter.php */

    /* @inject @var \Nette\Database\Control */
    public $db;

    public function createComponentBasket() {
        return new Basket($this->db);
    }

Doporučené použití: předání komponentě závislostí, které si nemůže sama obstarat. Např. přístup do databáze, k překladači (translatoru), atd.

Druhou možností je předat data v makru {control} a přečíst je v metodě render*():

/* @eshop.latte */
{layout '@layout.latte'}

{block content}
    {control basket 5, 'box' } {* parametry *}
    ...
{/block}
/* Basket.php */
class Basket
    extends \Nette\Application\UI\Control {

    public function render($count, $class) {
        $t = $this->template;
        ...
        $t->nakup = $this->getNakup($count);
        $t->class = $class;
        ...
    }

    protected function getNakup($count) {
        $this->db->query('... LIMIT ' . $count);
    }
}

Předání parametrů je snadné, i když trochu nepřehledné – když se podíváte do @eshop.latte, netušíte, co znamená 5 a box.

Doporučené použití: Předání parametrů, které určují, jak se komponenta vykreslí (např. kolik má vypsat položek nebo jaké HTML atributy má mít).

Pojmenované parametry komponenty

V předchozím příkladu je vidět, že předáváme parametry 5 a box, ale není jasné, co znamenají. Pro lepší porozumění kódu by bylo zajímavější parametry pojmenovat:

/* @eshop.latte */
{layout '@layout.latte'}

{block content}
    {control basket
         count => 5,
         class => 'box' }
    ...
{/block}

Samozřejmě to možné je, ale změní se způsob, jak data dostaneme do rendereru:

/* Basket.php */
class Basket
    extends \Nette\Application\UI\Control {

    public function render(array $params) {
        $t = $this->template;
        ...
        $t->nakup =
             $this->getNakup($params->count);
        $t->class = $params->class;
        ...
    }
}

Pokud nevíte, k čemu přávě došlo, tak to objasním. Latte, pokud najde šipku ‚=>‚ tam, kde očekává proměnné, automaticky všechny parametry vloží do pole. Toto pole pak dostaneme do metody render() jako jediný parametr typu pole (s pojmenovanými klíči). Zápis tedy odpovídá tomuhle:

    {control basket array(
         count => 5,
         class => 'box',
     )}

Doporučené použití: Komponenta, která vyžaduje předání několika parametrů (a prosté předání hodnot by bylo nejasné).

Volitelné parametry komponenty

Předchozí kód můžeme ještě vylepšit o to, že komponenta bude mít volitelné parametry a použije výchozí hodnoty, pokud je neuvedeme:

/* Basket.php */
class Basket
    extends \Nette\Application\UI\Control {

    protected $params = array(
        'count' => 10,
        'class' => '',
    );

    protected function getParams($params) {
        foreach ($this->params as $param) {
          $this->template->$param = (
            array_key_exists($param, $params)
              ? $params[$param]
              : $this->params[$param]
          );
        }
    }

    public function render($params = array()) {
        $this->getParams($params);
        ...
    }
}

Doporučení použití: Komponenta, u které můžete určit, jak se má vykreslit (ve speciálním případě), ale ve většině případů budete chtít, aby se vykreslila stejně jako na ostatních stránkách.

Předání bloku do šablony

Už jsme si ukázali, jak do šablony vložit jinou šablonu nebo blok. Co když ale potřebujete naopak vložit do samostatné šablony nebo komponenty jinou část kódu?

Pro příklad budeme chtít v šabloně nákupního vozíku (basket.latte) zobrazit informaci o servisu, kterou máme uloženu ve warranty.latte. Jistě nás napadne, že nejjednodušší by bylo warranty.latte naimportovat do basket.latte a je po problému.

Samozřejmě to jde, ale není to příliš promyšlené, protože když změníme warranty.latte (např. přejmenujeme blok servis na servisnimista), museli bychom měnit notebooky.latte i basket.latte. Jednodušší by bylo naimportovat warranty.latte do layoutu @eshop.latte a odtud předávat zvolené bloky do použitých šablon.

K tomu slouží makro {capture}, které funguje jako {block}, ale místo na výstup uloží výsledek do proměnné. Tu pak můžete používat jako jakoukoliv jinou proměnnou a buď ji vypsat nebo předat někam dál:

/* @eshop.latte */
{layout '@layout.latte'}

{* načteme bloky týkající se záruky *}
{import warranty.latte}

{block content}
    {capture $zaruka}
        Uplatnění záruky: {include servis}
    {/capture}

    {include 'basket.latte' info => $zaruka}
    ...
{/block}

V layoutu jsme si vytvořili blok, který se přímo nevypíše, ale uloží se do proměnné $zaruka. Pozor na to, že proměnná existuje jen uvnitř nadřazeného bloku (zde content). Makro {capture} musíte tedy vložit vždy do stejného bloku v jakém vkládáte šablonu, do které chcete proměnno předat!

Proměnná bude obsahovat čistý HTML kód (tedy String) a tak s ním také můžeme pracovat. Například bychom ho mohli vytisknout echo makrem {$zaruka}. My jsme ho ale předali do šablony, která ji následně bude moci vytisknout:

/* basket.latte */
{block basket}

<div>Nákupní košík</div>

<ul>
        ...
    </ul>

    <span n:ifset="$info">{$info}</span>
{/block}

Šablona basket.latte musí nejprve ověřit, že proměnná $info byla předána (makrem n:ifset="$info" nebo {ifset $info}) a pokud ano, může ji vytisknout.

Teď jsme pracovali s původním šablonovým košíkem. U komponenty Basket bychom parametr předali stejně. V komponentě by pak stačilo přidat "info" do seznamu volitelnných parametrů. Trochu by se lišila kontrola existence proměnné, protože díky volitelným parametrům bude $info vždy existovat. Můžeme ale použít makro n:ifcontent, které zjistí, že prázdný tag se nevytiskne:

/* basket.latte (voláno z Basket.php) */
{block basket}
    ...
    <span n:ifcontent>{$info}</span>
{/block}

Pro uložení bloku do proměnné a současného použití v šabloně budeme muset trochu kód upravit (kvůli tomu, jak Latte bloky kompiluje do PHP). Využijeme k tomu makro {define}, které vytvoří blok, ale nevypíše ho:

/* @eshop.latte */
{layout '@layout.latte'}

{* načteme bloky týkající se záruky *}
{import warranty.latte}

{* vše musí být uvnitř jednoho bloku!!! *}
{block content}
    {* vytvoření bloku se zárukou *}
    {define info}{include zaruka}{/define}

    {* uložení bloku do proměnné *}
    {capture $zaruka}{include info}{/capture}

    {* předání proměnné do šablony *}
    {include 'basket.latte' info => $zaruka}
    ...
{/block}
/* notebooky.latte */
...
{* použití bloku z layoutu ve výchozí šabloně *}
{block info}
    {include parent} {* vloží blok z define *}
{/block}

Doporučené použití: Když uvnitř šablony nebo komponenty potřebujeme zobrazit část stránky, která s ní ale přímo nesouvisí nebo obsahuje něco, co daná šablona nebo komponenta nemůže jinak získat. Nebo chcete v šabloně nebo komponentě zobrazit něco, co jste již jednou vyppisovali v nadřazené šabloně nebo layoutu.

Předání překladu do komponenty

Pokud bychom potřebovali v komponentě vypsat přeložený text, museli bychom do komponenty předat referenci na překladač (translator). To je samozřejmě možné (viz výše příklad s databází), ale pokud máme více komponent, může být únavné do každé předávat překladač.

Jednodušší je uložit si překlady z hlavní šablony do proměnných a pak je předávat do komponent:

/* @eshop.latte */
{layout '@layout.latte'}

{block content}
    {* získání překladů *}
    {capture $kosik}{_Shopping basket}{/capture}
    {capture $buy}{_Order and Pay}{/capture}

    {* předání překladů do komponenty *}
    {control basket
        title => $kosik,
        button => $buy
    }
    ...
{/block}

Předání komponenty

Teď víme, jak vložit blok do šablony nebo komponenty. Co když ale chceme vložit komponentu? Třeba kdybychom z warranty.latte udělali komponentu Warranty s medotou renderServis()?

Postup je stejný – komponentu si necháme vyrenderovat do bloku, ten uložíme do proměnné a předáme dál:

/* @eshop.latte */
{layout '@layout.latte'}

{block content}
    {* Uložíme komponentu do proměnné... *}
    {capture $zaruka}
         Servis: {control warranty:servis}
    {/capture}

    {* ... a předáme do komponenty *}
    {control basket limit => 5, info => $zaruka}
    ...
{/block}

Díky tomuto postupu jsme se vyhnuli tomu, abychom v komponentě Basket museli vytvářet továrnu na Warranty (kterou už stejné máme v EshopPresenter).

Vyhození výjimky

Pokud při generování šablony dojde k nějaké chybě (kterou detekuje programátor, např. chybějící parametr), je běžné vyhodit výjimku. S Nette pak máte tu výhodu, že se vám zobrazí laděnka (Tracy debugger), který vám vše podrobně popíše.

U rozdělených šablon ale vyhození výjimky (tedy throw new Exception()) v jedné části stránky může zbytečně zamezit uživateli používat ostatní části, které jsou v pořádku. Lepší by tedy bylo neblokovat celou stránku, ale jen vyhodit méně kritickou chybu (t.j. Warning) a nechat zbytek stránky normálně zobrazit:

/* basket.latte */
{block basket}
    {if !isset($nakup)}
        {trigger_error('Chybí nákup!',
               E_USER_WARNING)}
    {else}
    ... {* zbytek kódu košíku *}
    {/if}
{/block}

Nebo v komponentě:

/* Basket.php */
class Basket extends
       \Nette\Application\UI\Control { 

    public function render() {
        $nakup = $this->getNakup();
        if (is_null($nakup)) {
            trigger_error('Chybí nákup!',
                     E_USER_WARNING);
            return '';
        }
        ... //zbytek kódu košíku
    }

Pokud vyhodíte jen Warning (kvůli omezení trigger_error() je nutné použít E_USER_WARNING místo E_WARNING) a používáte laděnku (Tracy debugger), chyba se zobrazí jako červený blok na debug baru, ale stránka se normálně zobrazí (jen v ní bude chybět část, ve které došlo k chybě). Na produkci se pak chyba zapíše do error.log a stránka se normálně zobrazí (opět bez chybné části). Pokud laděnku nepoužíváte (čímž se okrádáte o užitečné informace), chyba se pravděpodobně přímo vypíše do stránky místo chybné části nebo taky ne (v závislosti na nastavení PHP).

Nevýhoda tohoto přístupu je v tom, že z Warningu se vypíše jen zpráva a řádek, kde jste ji vyhodili. Nezjistíte už ale, odkud se šablona nebo komponenta používala (stack trace) nebo v jakém requestu k tomu došlo (environment).

Komponentní CSS

Jestli vám stále unikal důvod, proč píšu na CSS blogu o HTML šablonách, důvodem je komponentní CSS.

Když totiž šablonu správně rozdělíte na menší celky, každá část může mít vlastní CSS soubor, který si do kódu vloží jen tehdy, když ho bude potřebovat. Uživatel tak nebude muset čekat na stažení všech CSS před tím, než se mu zobrazí hlavička stránky.

Například výše uvedená šablona/komponenta pro nákupní košík si může vložit vlastní CSS a pokud na stránce košík nebude, nebude se stahovat ani jeho CSS:

/* basket.latte */
{block basket}
<link rel="stylesheet" href="/basket.css">

<div Nákupní košík</div>

<ul>

<li n:foreach="$nakup as $i">
         {include 'basketItem.latte'
            item => $i
         }
    </li>

</ul>

{/block}

Pokud navíc chcete, aby se každé CSS vložilo jen jednou, můžete použít vlastní makro, které to zajistí:

class CssMacro extends \Latte\Macros\MacroSet
{
    public static $cache = [];
    public static function install(
              \Latte\Compiler $compiler)
    {
        $me = new static($compiler);
        $me->addMacro('css',
              array($me, 'addCss'));
    }

    public function addCss(
            \Latte\MacroNode $node,
            \Latte\PhpWriter $writer)
    {
        $css = '/css/' . $node->args . '.css';
        return <<<WRITE_CSS_MACRO_CODE
        if (!array_key_exists('${css}',
                CssMacro::\$cache)) {
            CssMacro::\$cache['${css}'] = true;
            echo '<link rel="stylesheet"' . ' href="${css}" />';
        }
WRITE_CSS_MACRO_CODE;
    }
}

Makro si pak do Nette vložíme přes konfigurační soubor (NEON):

latte:
    macros:
        - CssMacro

Nebo pokud používáte Latte bez Nette frameworku:

$latte = new Latte\Engine;
CssMacro::install($latte->compiler);
$latte->render('notebooky.latte');

Použití už je pak jednoduché:

/* basket.latte */
{block basket}
    {css basket}
    {* vloží /css/basket.css *}

    ...
{/block}

2 komentáře u „Latte jako od baristky“

  1. Zdravím, díky za článek, chtěl bych ale poprosit o radu – člověk co nám dělal stránky už moc není dostupný a po úpravě textu webu v souborech @layout.latte (/www/app/FrontModule/templates) a default.latte (/www/app/FrontModule/templates/Homepage) se mi změny neprojevily a text na webu je pořád stejný. Při zpětné kontrole (stažení upravených souborů z FTP) ale vše vypadá jak by mělo. Tušíte prosím někdo kde je problém?

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *

Tato stránka používá Akismet k omezení spamu. Podívejte se, jak vaše data z komentářů zpracováváme..