Dziedziczenie szablonów i ponowne wykorzystanie

Mechanizmy ponownego wykorzystania i dziedziczenia szablonów są tu po to, aby zwiększyć Twoją produktywność: każdy szablon zawiera tylko swoją unikalną treść, a powtarzające się elementy i struktury są wykorzystywane ponownie. Przedstawiamy trzy koncepcje: Dziedziczenie layoutu, Poziome wykorzystanieDziedziczenie jednostkowe.

Koncepcja dziedziczenia szablonów Latte przypomina dziedziczenie klas w PHP. Definiujesz szablon nadrzędny, po którym inne szablony potomne mogą dziedziczyć i nadpisywać jego części. Świetnie sprawdza się to, gdy elementy mają wspólną strukturę. Brzmi skomplikowanie? Bez obaw, to bardzo proste.

Dziedziczenie layoutu {layout}

Przyjrzyjmy się dziedziczeniu layoutu na przykładzie. Oto szablon nadrzędny, nazwijmy go layout.latte, który definiuje szkielet dokumentu HTML:

<!doctype html>
<html lang="en">
<head>
	<title>{block title}{/block}</title>
	<link rel="stylesheet" href="style.css">
</head>
<body>
	<div id="content">
		{block content}{/block}
	</div>
	<div id="footer">
		{block footer}&copy; Copyright 2008{/block}
	</div>
</body>
</html>

Tagi {block} definiują trzy bloki, które mogą wypełnić szablony potomne. Wszystko, co robi tag block, to informuje silnik szablonów, że szablon potomny może nadpisać te fragmenty, definiując własny blok o tej samej nazwie.

Szablon potomny może wyglądać tak:

{layout 'layout.latte'}

{block title}My amazing blog{/block}

{block content}
	<p>Welcome to my awesome homepage.</p>
{/block}

Kluczowy jest tutaj tag {layout}. Mówi Latte, że ten szablon “rozszerza” inny szablon. Gdy Latte renderuje ten szablon, najpierw odnajduje szablon nadrzędny, w tym przypadku layout.latte.

W tym momencie Latte zauważa trzy tagi block w layout.latte i zastępuje te bloki treścią szablonu potomnego. Ponieważ szablon potomny nie zdefiniował bloku footer, użyta zostaje treść z szablonu nadrzędnego. Treść wewnątrz tagu {block} w szablonie nadrzędnym zawsze służy jako wartość zapasowa.

Wynik może wyglądać tak:

<!doctype html>
<html lang="en">
<head>
	<title>My amazing blog</title>
	<link rel="stylesheet" href="style.css">
</head>
<body>
	<div id="content">
		<p>Welcome to my awesome homepage.</p>
	</div>
	<div id="footer">
		&copy; Copyright 2008
	</div>
</body>
</html>

W szablonie potomnym bloki umieszcza się zwykle na najwyższym poziomie albo wewnątrz innego bloku, na przykład:

{block content}
	<h1>{block title}Welcome to my awesome homepage{/block}</h1>
{/block}

Blok powstanie też zawsze niezależnie od tego, czy otaczający go warunek {if} zostanie obliczony jako prawdziwy, czy fałszywy. Choć więc może się tak nie wydawać, ten szablon blok definiuje.

{if false}
	{block head}
		<meta name="robots" content="noindex, follow">
	{/block}
{/if}

Jeśli chcesz, aby wynik wewnątrz bloku wyświetlał się warunkowo, użyj zamiast tego:

{block head}
	{if $condition}
		<meta name="robots" content="noindex, follow">
	{/if}
{/block}

Kod w nagłówku szablonu potomnego (czyli przed pierwszym blokiem albo jakimkolwiek wyjściem) wykonuje się przed wyrenderowaniem szablonu layoutu, możesz więc użyć go do zdefiniowania zmiennych, jak {var $foo = bar}, i przekazywać dane w całym łańcuchu dziedziczenia. Kod umieszczony między blokami albo za nimi w szablonie z {layout} nie wykonuje się wcale:

{layout 'layout.latte'}
{var $robots = noindex}

...

Jeśli chcesz przekazać zmienne wyłącznie do layoutu, nie tworząc ich w bieżącym szablonie, wypisz je bezpośrednio w tagu {layout} (albo {extends}) po przecinku:

{layout 'layout.latte', robots: noindex}

Zmienna $robots będzie dostępna w layoucie i jego blokach, ale nie w blokach bieżącego szablonu. Zmienna przekazana jawnie ma też pierwszeństwo przed parametrem szablonu o tej samej nazwie.

Dziedziczenie wielopoziomowe

Możesz użyć tylu poziomów dziedziczenia, ile potrzebujesz. Częstym sposobem wykorzystania dziedziczenia layoutu jest następujące podejście trzypoziomowe:

  1. Utwórz szablon layout.latte, który zawiera główny wygląd Twojej witryny.
  2. Utwórz szablon layout-SECTIONNAME.latte dla każdej sekcji witryny. Na przykład layout-news.latte, layout-blog.latte itd. Wszystkie te szablony rozszerzają layout.latte i zawierają style oraz elementy graficzne charakterystyczne dla danej sekcji.
  3. Utwórz osobne szablony dla każdego typu strony, na przykład artykułu z newsów albo wpisu na blogu. Te szablony rozszerzają odpowiedni szablon sekcji.

Dynamiczne dziedziczenie layoutu

Jako nazwy szablonu nadrzędnego możesz użyć zmiennej albo dowolnego wyrażenia PHP, dzięki czemu dziedziczenie może zachowywać się dynamicznie:

{layout $standalone ? 'minimum.latte' : 'layout.latte'}

Możesz też skorzystać z API Latte, aby wybierać szablon layoutu automatycznie.

Wskazówki

Oto kilka wskazówek dotyczących pracy z dziedziczeniem layoutu:

  • Jeśli używasz w szablonie {layout}, musi on znaleźć się w nagłówku szablonu, czyli przed jakimkolwiek wyjściem. Wyprzedzać go mogą tylko tagi, które nie produkują wyjścia (jak {var}, {templateType}, {import} czy komentarze).
  • Layout może być wyszukiwany automatycznie (jak w presenterach). Jeśli w takim przypadku szablon nie ma mieć layoutu, sygnalizuje to tagiem {layout none}. Odwrotnie, {layout auto} (albo {extends auto}) przywraca automatyczne wyszukiwanie layoutu.
  • Tag {layout} ma alias {extends}.
  • Nazwa pliku layoutu zależy od loadera.
  • Bloków możesz mieć dowolnie wiele. Pamiętaj, że szablony potomne nie muszą definiować wszystkich bloków nadrzędnych, możesz więc w kilku blokach ustawić rozsądne wartości domyślne, a później zdefiniować tylko te, których potrzebujesz.

Bloki {block}

Zobacz też anonimowy {block}

Blok daje możliwość zmiany sposobu, w jaki renderowana jest pewna część szablonu, ale w żaden sposób nie ingeruje w logikę wokół niego. Pokażmy na poniższym przykładzie, jak blok działa, a co ważniejsze, jak nie działa:

{foreach $posts as $post}
{block post}
	<h1>{$post->title}</h1>
	<p>{$post->body}</p>
{/block}
{/foreach}

Jeśli wyrenderujesz ten szablon, wynik będzie dokładnie taki sam z tagami {block} i bez nich. Bloki mają dostęp do zmiennych z zewnętrznych zasięgów. Dają jedynie możliwość nadpisania przez szablon potomny:

{layout 'parent.latte'}

{block post}
	<article>
		<header>{$post->title}</header>
		<section>{$post->text}</section>
	</article>
{/block}

Teraz przy renderowaniu szablonu potomnego pętla użyje bloku zdefiniowanego w szablonie potomnym child.latte zamiast tego z parent.latte; wykonywany szablon jest wtedy równoważny poniższemu:

{foreach $posts as $post}
	<article>
		<header>{$post->title}</header>
		<section>{$post->text}</section>
	</article>
{/foreach}

Jeśli jednak wewnątrz nazwanego bloku utworzymy nową zmienną albo zamienimy wartość istniejącej, zmiana będzie widoczna tylko wewnątrz bloku:

{var $foo = 'foo'}
{block post}
	{do $foo = 'new value'}
	{var $bar = 'bar'}
{/block}

foo: {$foo}                  // wypisze: foo
bar: {$bar ?? 'not defined'} // wypisze: not defined

Treść bloku można modyfikować filtrami. Poniższy przykład usuwa cały HTML i zamienia treść na wielkie litery:

<title>{block title|stripHtml|capitalize}...{/block}</title>

Tag można zapisać także jako n:atrybut:

<article n:block=post>
	...
</article>

Bloki lokalne

Każdy blok nadpisuje treść bloku nadrzędnego o tej samej nazwie, z wyjątkiem bloków lokalnych. Są one odpowiednikiem metod prywatnych w klasach. Możesz utworzyć szablon bez obaw, że przez zbieżność nazw bloków zostaną one nadpisane przez inny szablon.

{block local helper}
	...
{/block}

Renderowanie bloków {include}

Zobacz też {include file}

Aby wyrenderować blok w konkretnym miejscu, użyj tagu {include blockname}:

<title>{block title}{/block}</title>

<h1>{include title}</h1>

Możesz też wyrenderować blok z innego szablonu:

{include footer from 'main.latte'}

Renderowany blok nie ma dostępu do zmiennych aktywnego kontekstu, chyba że jest zdefiniowany w tym samym pliku, w którym jest wstawiany. Ma natomiast dostęp do zmiennych globalnych.

Zmienne możesz przekazać do bloku tak:

{include footer, foo: bar, id: 123}

Nazwą bloku może być zmienna albo dowolne wyrażenie PHP. W takim przypadku dodaj przed zmienną słowo kluczowe block, aby Latte wiedziało już w czasie kompilacji, że chodzi o blok, a nie o dołączany szablon, którego nazwa również mogłaby być w zmiennej:

{var $name = footer}
{include block $name}

Blok można też wyrenderować wewnątrz niego samego, co przydaje się na przykład przy renderowaniu struktury drzewiastej:

{define menu, $items}
<ul>
	{foreach $items as $item}
		<li>
		{if is_array($item)}
			{include menu, $item}
		{else}
			{$item}
		{/if}
		</li>
	{/foreach}
</ul>
{/define}

Zamiast {include menu, ...} możemy napisać też {include this, ...}, gdzie this oznacza bieżący blok.

Renderowaną treść bloku można modyfikować filtrami. Poniższy przykład usuwa cały HTML i zamienia treść na wielkie litery:

<title>{include heading|stripHtml|capitalize}</title>

Blok nadrzędny

Jeśli potrzebujesz wyrenderować treść bloku z szablonu nadrzędnego, użyj {include parent}. Przydaje się to, gdy chcesz uzupełnić treść bloku nadrzędnego, zamiast całkowicie ją nadpisywać.

{block footer}
	{include parent}
	<a href="https://github.com/nette">GitHub</a>
	<a href="https://twitter.com/nettefw">Twitter</a>
{/block}

Definicje {define}

Oprócz bloków Latte ma również “definicje”. W typowych językach programowania byłyby one porównywalne z funkcjami. Przydają się do ponownego wykorzystywania fragmentów szablonu, aby uniknąć powtórzeń.

Latte stara się zachować prostotę, więc w gruncie rzeczy definicje są tym samym co bloki i wszystko, co powiedziano o blokach, dotyczy również definicji. Od bloków różnią się tym, że:

  1. są zamknięte w tagach {define}
  2. renderują się dopiero po wstawieniu przez {include}
  3. możesz zdefiniować dla nich parametry, podobnie jak dla funkcji w PHP
{block foo}<p>Hello</p>{/block}
{* wypisze: <p>Hello</p> *}

{define bar}<p>World</p>{/define}
{* nie wypisze nic *}

{include bar}
{* wypisze: <p>World</p> *}

Wyobraź sobie, że masz pomocniczy szablon ze zbiorem definicji tego, jak rysować formularze HTML.

{define input, $name, $value, $type = 'text'}
	<input type={$type} name={$name} value={$value}>
{/define}

{define textarea, $name, $value}
	<textarea name={$name}>{$value}</textarea>
{/define}

Argumenty są zawsze opcjonalne, z wartością domyślną null, o ile nie podano wartości domyślnej (tutaj 'text' jest wartością domyślną dla $type). Można też zadeklarować typy parametrów: {define input, string $name, ...}.

Szablon z definicjami wczytuje się przez {import}. Same definicje renderuje się tak samo jak bloki:

<p>{include input, 'password', null, 'password'}</p>
<p>{include textarea, 'comment'}</p>

Tak jak bloki, definicje nie mają dostępu do zmiennych aktywnego kontekstu, a jedynie do zmiennych globalnych. Wyjątkiem jest definicja, która nie ma zadeklarowanych parametrów, jest przywoływana pod statyczną nazwą i wstawiana w tym samym pliku, w którym została zdefiniowana: taka definicja ma dostęp do zmiennych kontekstu miejsca, z którego jest wstawiana.

Dynamiczne nazwy bloków

Latte daje dużą elastyczność w definiowaniu bloków, bo nazwą bloku może być dowolne wyrażenie PHP. Ten przykład definiuje trzy bloki o nazwach hi-Peter, hi-John i hi-Mary:

{foreach [Peter, John, Mary] as $name}
	{block "hi-$name"}Hi, I am {$name}.{/block}
{/foreach}

W szablonie potomnym możemy potem przedefiniować na przykład tylko jeden blok:

{block hi-John}Hello. I am {$name}.{/block}

Wynik będzie więc wyglądał tak:

Hi, I am Peter.
Hello. I am John.
Hi, I am Mary.

Sprawdzanie istnienia bloku {ifset}

Zobacz też {ifset $var}

Aby sprawdzić, czy w bieżącym kontekście istnieje blok (albo kilka bloków), użyj testu {ifset blockname}:

{ifset footer}
	...
{/ifset}

{ifset footer, header, main}
	...
{/ifset}

Nazwą bloku może być zmienna albo dowolne wyrażenie PHP. W takim przypadku dodaj przed zmienną słowo kluczowe block, aby wyjaśnić, że nie chodzi o sprawdzenie istnienia zmiennych:

{ifset block $name}
	...
{/ifset}

Istnienie bloków sprawdza również funkcja hasBlock():

{if hasBlock(header) || hasBlock(footer)}
	...
{/if}

Wskazówki

Kilka wskazówek dotyczących pracy z blokami:

  • Ostatni blok najwyższego poziomu nie potrzebuje tagu zamykającego (blok kończy się wraz z końcem dokumentu). Upraszcza to pisanie szablonów potomnych zawierających jeden główny blok.
  • Dla lepszej czytelności możesz opcjonalnie podać nazwę bloku w tagu {/block}, na przykład {/block footer}. Nazwa musi jednak odpowiadać nazwie bloku. W większych szablonach ta technika pomaga zobaczyć, które tagi block są zamykane.
  • W tym samym szablonie nie możesz bezpośrednio zdefiniować kilku tagów block o tej samej nazwie. Da się to jednak osiągnąć za pomocą Dynamiczne nazwy bloków.
  • Bloki możesz definiować za pomocą n:atrybutów, na przykład <h1 n:block=title>Welcome to my awesome homepage</h1>
  • Bloków można też używać bez nazw, wyłącznie po to, aby zastosować do wyniku filtry{block|strip} hello {/block}

Poziome wykorzystanie {import}

Poziome wykorzystanie to trzeci mechanizm ponownego wykorzystania i dziedziczenia w Latte. Pozwala wczytywać bloki z innych szablonów. Przypomina to utworzenie w PHP pliku z funkcjami pomocniczymi i wczytanie go przez require.

Dziedziczenie layoutu jest jedną z najpotężniejszych możliwości Latte, ale ogranicza się do prostego dziedziczenia: szablon może rozszerzać tylko jeden inny szablon. Poziome wykorzystanie to sposób na osiągnięcie dziedziczenia wielokrotnego.

Weźmy plik z definicjami bloków:

{block sidebar}...{/block}

{block menu}...{/block}

Za pomocą polecenia {import} importujemy do innego szablonu wszystkie bloki i definicje zdefiniowane w blocks.latte:

{import 'blocks.latte'}

{* bloków sidebar i menu można teraz używać *}

Jeśli zaimportujesz bloki w szablonie nadrzędnym (czyli użyjesz {import} w layout.latte), bloki będą dostępne również we wszystkich szablonach potomnych, co jest bardzo praktyczne.

Szablon przeznaczony do zaimportowania (np. blocks.latte) nie może rozszerzać innego szablonu, czyli używać {layout}. Może natomiast importować inne szablony.

Tag {import} powinien być pierwszym tagiem szablonu po {layout}. Nazwą szablonu może być dowolne wyrażenie PHP:

{import $ajax ? 'ajax.latte' : 'not-ajax.latte'}

W szablonie możesz użyć dowolnie wielu poleceń {import}. Jeśli dwa importowane szablony definiują ten sam blok, wygrywa pierwszy. Najwyższy priorytet ma jednak szablon główny, który może nadpisać dowolny zaimportowany blok.

Tag {import} może też przekazać do importowanego szablonu argumenty, na przykład {import 'blocks.latte', foo: 1}. Argumenty te są potem dostępne jako zmienne w importowanych blokach i definicjach.

Treść nadpisanych bloków można zachować, wstawiając blok tak samo jak blok nadrzędny:

{layout 'layout.latte'}

{import 'blocks.latte'}

{block sidebar}
	{include parent}
{/block}

{block title}...{/block}
{block content}...{/block}

W tym przykładzie {include parent} wywołuje blok sidebar z szablonu blocks.latte.

Dziedziczenie jednostkowe {embed}

Dziedziczenie jednostkowe rozszerza ideę dziedziczenia layoutu na poziom fragmentów treści. Podczas gdy dziedziczenie layoutu pracuje ze “szkieletami dokumentów”, które ożywiają szablony potomne, dziedziczenie jednostkowe pozwala tworzyć szkielety mniejszych jednostek treści i wykorzystywać je wszędzie tam, gdzie chcesz.

W dziedziczeniu jednostkowym kluczowy jest tag {embed}. Łączy zachowanie {include} i {layout}. Pozwala osadzić treść innego szablonu albo bloku i opcjonalnie przekazać zmienne, tak jak {include}. Pozwala też nadpisać dowolny blok zdefiniowany wewnątrz osadzanego szablonu, jak {layout}.

Weźmy na przykład element typu accordion. Spójrz na szkielet elementu zapisany w szablonie collapsible.latte:

<section class="collapsible {$modifierClass}">
	<h4 class="collapsible__title">
		{block title}{/block}
	</h4>

	<div class="collapsible__content">
		{block content}{/block}
	</div>
</section>

Tagi {block} definiują dwa bloki, które mogą wypełnić szablony potomne. Tak, dokładnie jak w przypadku szablonu nadrzędnego przy dziedziczeniu layoutu. Widzisz też zmienną $modifierClass.

Użyjmy naszego elementu w szablonie. Tu wkracza {embed}. To niezwykle potężny tag, który pozwala nam zrobić to wszystko: osadzić treść szablonu elementu, dodać do niej zmienne i dodać do niej bloki z własnym HTML:

{embed 'collapsible.latte', modifierClass: my-style}
	{block title}
		Hello World
	{/block}

	{block content}
		<p>Lorem ipsum dolor sit amet, consectetuer adipiscing
		elit. Nunc dapibus tortor vel mi dapibus sollicitudin.</p>
	{/block}
{/embed}

Wynik może wyglądać tak:

<section class="collapsible my-style">
	<h4 class="collapsible__title">
		Hello World
	</h4>

	<div class="collapsible__content">
		<p>Lorem ipsum dolor sit amet, consectetuer adipiscing
		elit. Nunc dapibus tortor vel mi dapibus sollicitudin.</p>
	</div>
</section>

Bloki wewnątrz tagów embed tworzą osobną warstwę, odizolowaną od bloków poza embedem. Mogą więc nosić tę samą nazwę co blok na zewnątrz, nie kolidując z nim i nie podlegając jego wpływowi. Za pomocą tagu include wewnątrz tagów {embed} możesz wstawiać bloki utworzone tutaj, bloki z osadzanego szablonu (które nie są lokalne), a także bloki z szablonu głównego, które lokalne. Możesz też zaimportować bloki z innych plików:

{block outer}…{/block}
{block local hello}…{/block}

{embed 'collapsible.latte', modifierClass: my-style}
	{import 'blocks.latte'}

	{block inner}…{/block}

	{block title}
		{include inner} {* działa, blok jest zdefiniowany wewnątrz embed *}
		{include hello} {* działa, blok jest lokalny w tym szablonie *}
		{include content} {* działa, blok jest zdefiniowany w osadzanym szablonie *}
		{include aBlockDefinedInImportedTemplate} {* działa *}
		{include outer} {* nie działa! - blok jest w warstwie zewnętrznej *}
	{/block}
{/embed}

Osadzane szablony nie mają dostępu do zmiennych aktywnego kontekstu, mają natomiast dostęp do zmiennych globalnych.

Za pomocą {embed} możesz osadzać nie tylko szablony, ale też inne bloki, więc poprzedni przykład można by zapisać tak:

{define collapsible}
<section class="collapsible {$modifierClass}">
	<h4 class="collapsible__title">
		{block title}{/block}
	</h4>
	...
</section>
{/define}


{embed collapsible, modifierClass: my-style}
	{block title}
		Hello World
	{/block}
	...
{/embed}

Jest jednak między nimi jedna różnica: gdy osadzasz blok zamiast pliku, bloki z warstwy zewnętrznej pozostają wewnątrz embed dostępne. W odróżnieniu od osadzanego pliku {include outer} zadziałałoby tam więc bez problemu.

Jeśli przekażemy do {embed} wyrażenie i nie jest jasne, czy chodzi o nazwę bloku, czy o nazwę pliku, dodaj słowo kluczowe block albo file:

{embed block $name} ... {/embed}

Przypadki użycia

W Latte istnieją różne rodzaje dziedziczenia i ponownego wykorzystania kodu. Dla większej przejrzystości podsumujmy główne koncepcje:

{include template}

Przypadek użycia: Użycie header.latte i footer.latte wewnątrz layout.latte.

header.latte

<nav>
   <div>Home</div>
   <div>About</div>
</nav>

footer.latte

<footer>
   <div>Copyright</div>
</footer>

layout.latte

{include 'header.latte'}

<main>{block main}{/block}</main>

{include 'footer.latte'}

{layout}

Przypadek użycia: Rozszerzanie layout.latte w homepage.latte i about.latte.

layout.latte

{include 'header.latte'}

<main>{block main}{/block}</main>

{include 'footer.latte'}

homepage.latte

{layout 'layout.latte'}

{block main}
	<p>Homepage</p>
{/block}

about.latte

{layout 'layout.latte'}

{block main}
	<p>About page</p>
{/block}

{import}

Przypadek użycia: Użycie sidebar.latte w single.product.latte i single.service.latte.

sidebar.latte

{block sidebar}<aside>This is sidebar</aside>{/block}

single.product.latte

{layout 'product.layout.latte'}

{import 'sidebar.latte'}

{block main}<main>Product page</main>{/block}

single.service.latte

{layout 'service.layout.latte'}

{import 'sidebar.latte'}

{block main}<main>Service page</main>{/block}

{define}

Przypadek użycia: Funkcje, które przyjmują zmienne i coś renderują.

form.latte

{define form-input, $name, $value, $type = 'text'}
	<input type={$type} name={$name} value={$value}>
{/define}

profile.service.latte

{import 'form.latte'}

<form action="" method="post">
	<div>{include form-input, username}</div>
	<div>{include form-input, password}</div>
	<div>{include form-input, submit, Submit, submit}</div>
</form>

{embed}

Przypadek użycia: Osadzenie pagination.latte w product.table.latte i service.table.latte.

pagination.latte

<div id="pagination">
	<div>{block first}{/block}</div>

	{for $i = $min + 1; $i < $max - 1; $i++}
		<div>{$i}</div>
	{/for}

	<div>{block last}{/block}</div>
</div>

product.table.latte

{embed 'pagination.latte', min: 1, max: $products->count}
	{block first}First Product Page{/block}
	{block last}Last Product Page{/block}
{/embed}

service.table.latte

{embed 'pagination.latte', min: 1, max: $services->count}
	{block first}First Service Page{/block}
	{block last}Last Service Page{/block}
{/embed}
wersja: 3.x