Dziedziczenie i ponowne wykorzystanie szablonów

Mechanizmy ponownego wykorzystania i dziedziczenia szablonów zwiększą Twoją produktywność, ponieważ każdy szablon zawiera tylko swoją unikalną treść, a powtarzające się elementy i struktury są ponownie wykorzystywane. Przedstawiamy trzy koncepcje: Dziedziczenie layoutu, Ponowne wykorzystanie poziomeDziedziczenie jednostkowe.

Koncepcja dziedziczenia szablonów Latte jest podobna do dziedziczenia klas w PHP. Definiujesz szablon nadrzędny, z którego mogą dziedziczyć inne szablony podrzędne i mogą nadpisywać części szablonu nadrzędnego. Działa to świetnie, gdy elementy mają wspólną strukturę. Brzmi skomplikowanie? Nie martw się, to bardzo proste.

Dziedziczenie layoutu {layout}

Spójrzmy na dziedziczenie szablonu układu, czyli layoutu, od razu na przykładzie. To jest szablon nadrzędny, który będziemy nazywać na przykład layout.latte i 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 podrzędne. Tag block robi tylko to, że informuje, że to miejsce może nadpisać szablon podrzędny, definiując własny blok o tej samej nazwie.

Szablon podrzędny może wyglądać tak:

{layout 'layout.latte'}

{block title}My amazing blog{/block}

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

Kluczem jest tutaj tag {layout}. Mówi on Latte, że ten szablon „rozszerza” inny szablon. Kiedy Latte renderuje ten szablon, najpierw znajduje szablon nadrzędny – w tym przypadku layout.latte.

W tym momencie Latte zauważa trzy tagi blokowe w layout.latte i zastępuje te bloki zawartością szablonu podrzędnego. Ponieważ szablon podrzędny nie zdefiniował bloku footer, używana jest zamiast tego zawartość z szablonu nadrzędnego. Zawartość w tagu {block} w szablonie nadrzędnym jest zawsze używana jako 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 podrzędnym bloki mogą być umieszczone tylko na najwyższym poziomie lub wewnątrz innego bloku, tj.:

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

Również blok zostanie zawsze utworzony bez względu na to, czy otaczający warunek {if} jest oceniany jako prawdziwy czy fałszywy. Więc nawet jeśli tak nie wygląda, ten szablon zdefiniuje blok.

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

Jeśli chcesz, aby wyjście wewnątrz bloku wyświetlało się warunkowo, użyj zamiast tego następującego kodu:

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

Przestrzeń poza blokami w szablonie podrzędnym jest wykonywana przed renderowaniem szablonu layoutu, więc możesz jej użyć do definiowania zmiennych jak {var $foo = bar} i do propagowania danych w całym łańcuchu dziedziczenia:

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

...

Dziedziczenie wielopoziomowe

Możesz używać tylu poziomów dziedziczenia, ile potrzebujesz. Powszechnym sposobem użycia dziedziczenia layoutu jest następujące podejście trójpoziomowe:

  1. Utwórz szablon layout.latte, który zawiera główny szkielet wyglądu strony.
  2. Utwórz szablon layout-SECTIONNAME.latte dla każdej sekcji swojej strony. Na przykład layout-news.latte, layout-blog.latte itp. Wszystkie te szablony rozszerzają layout.latte i zawierają style i design specyficzne dla poszczególnych sekcji.
  3. Utwórz indywidualne szablony dla każdego typu strony, na przykład artykułu prasowego lub wpisu na blogu. Te szablony rozszerzają odpowiedni szablon sekcji.

Dynamiczne dziedziczenie

Jako nazwa szablonu nadrzędnego można użyć zmiennej lub dowolnego wyrażenia PHP, więc dziedziczenie może zachowywać się dynamicznie:

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

Możesz także użyć Latte API do automatycznego wyboru szablonu layoutu.

Wskazówki

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

  • Jeśli w szablonie użyjesz {layout}, musi to być pierwszy tag szablonu w tym szablonie.
  • Layout może być wyszukiwany automatycznie (jak na przykład w presenterach). W takim przypadku, jeśli szablon nie ma mieć layoutu, informuje o tym tagiem {layout none}.
  • Tag {layout} ma alias {extends}.
  • Nazwa pliku layoutu zależy od loadera.
  • Możesz mieć tyle bloków, ile chcesz. Pamiętaj, że szablony podrzędne nie muszą definiować wszystkich bloków nadrzędnych, więc możesz wypełnić rozsądne wartości domyślne w kilku blokach, a następnie zdefiniować tylko te, których potrzebujesz później.

Bloki {block}

Zobacz także anonimowy {block}

Blok reprezentuje sposób na zmianę sposobu renderowania określonej części szablonu, ale w żaden sposób nie ingeruje w logikę wokół niego. W poniższym przykładzie pokażemy, jak blok działa, ale także 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 zakresów. Dają tylko możliwość nadpisania przez szablon podrzędny:

{layout 'parent.Latte'}

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

Teraz podczas renderowania szablonu podrzędnego pętla będzie używać bloku zdefiniowanego w szablonie podrzędnym child.Latte zamiast bloku zdefiniowanego w parent.Latte; uruchomiony szablon jest wtedy równoważny następującemu:

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

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

{var $foo = 'foo'}
{block post}
	{do $foo = 'nowa wartość'}
	{var $bar = 'bar'}
{/block}

foo: {$foo}                  // drukuje: foo
bar: {$bar ?? 'niezdefiniowane'} // drukuje: niezdefiniowane

Zawartość bloku można modyfikować za pomocą filtrów. Poniższy przykład usuwa wszystkie znaczniki HTML i zmienia wielkość liter:

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

Tag można również zapisać jako n:atrybut:

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

Bloki lokalne

Każdy blok nadpisuje zawartość bloku nadrzędnego o tej samej nazwie – z wyjątkiem bloków lokalnych. W klasach byłoby to coś w rodzaju metod prywatnych. Możesz więc tworzyć szablon bez obaw, że z powodu zgodności nazw bloków zostaną one nadpisane z innego szablonu.

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

Renderowanie bloków {include}

Zobacz także {include file}

Aby wyświetlić blok w określonym miejscu, użyj tagu {include blockname}:

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

<h1>{include title}</h1>

Można również wyświetlić blok z innego szablonu:

{include footer from 'main.latte'}

Renderowany blok nie ma dostępu do zmiennych aktywnego kontekstu, z wyjątkiem przypadków, gdy blok jest zdefiniowany w tym samym pliku, w którym jest dołączany. Ma jednak dostęp do zmiennych globalnych.

Zmienne można przekazywać do bloku w ten sposób:

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

Jako nazwa bloku można użyć zmiennej lub dowolnego wyrażenia w PHP. W takim przypadku przed zmienną dodajemy jeszcze słowo kluczowe block, aby już w czasie kompilacji Latte wiedziało, że chodzi o blok, a nie o dołączanie szablonu, którego nazwa również mogłaby być w zmiennej:

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

Blok można renderować również wewnątrz siebie, co jest na przykład przydatne 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 wtedy napisać {include this, ...}, gdzie this oznacza bieżący blok.

Renderowany blok można modyfikować za pomocą filtrów. Poniższy przykład usuwa wszystkie znaczniki HTML i zmienia wielkość liter:

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

Blok nadrzędny

Jeśli potrzebujesz wyświetlić zawartość bloku z szablonu nadrzędnego, użyj {include parent}. Jest to przydatne, jeśli chcesz tylko uzupełnić zawartość bloku nadrzędnego zamiast jego całkowitego nadpisania.

{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 istnieją w Latte również „definicje”. W zwykłych językach programowania porównalibyśmy je do funkcji. Są przydatne do ponownego wykorzystania fragmentów szablonu, aby się nie powtarzać.

Latte stara się robić rzeczy prosto, więc w zasadzie definicje są takie same jak bloki i wszystko, co powiedziano o blokach, dotyczy również definicji. Różnią się od bloków tym, że:

  1. są zamknięte w tagach {define}
  2. renderują się dopiero, gdy je dołączysz przez {include}
  3. można im definiować parametry podobnie jak funkcjom w PHP
{block foo}<p>Witaj</p>{/block}
{* drukuje: <p>Witaj</p> *}

{define bar}<p>Świecie</p>{/define}
{* nic nie drukuje *}

{include bar}
{* drukuje: <p>Świecie</p> *}

Wyobraź sobie, że masz szablon pomocniczy z kolekcją definicji, 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 domyślną wartością null, chyba że podano wartość domyślną (tutaj 'text' jest wartością domyślną dla $type). Można również zadeklarować typy parametrów: {define input, string $name, ...}.

Szablon z definicjami ładujemy za pomocą {import}. Same definicje renderują się w ten sam sposób co bloki:

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

Definicje nie mają dostępu do zmiennych aktywnego kontekstu, ale mają dostęp do zmiennych globalnych.

Dynamiczne nazwy bloków

Latte pozwala na dużą elastyczność w definiowaniu bloków, ponieważ nazwa bloku może być dowolnym wyrażeniem 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 podrzędnym możemy wtedy przedefiniować na przykład tylko jeden blok:

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

Więc wynik będzie wyglądał tak:

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

Sprawdzanie istnienia bloków {ifset}

Zobacz także {ifset $var}

Za pomocą testu {ifset blockname} sprawdzamy, czy w bieżącym kontekście blok (lub więcej bloków) istnieje:

{ifset footer}
	...
{/ifset}

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

Jako nazwa bloku można użyć zmiennej lub dowolnego wyrażenia w PHP. W takim przypadku przed zmienną dodajemy jeszcze słowo kluczowe block, aby było jasne, że nie chodzi o test 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 musi mieć tagu zamykającego (blok kończy się końcem dokumentu). To upraszcza pisanie szablonów podrzędnych, które zawierają jeden główny blok.
  • Dla lepszej czytelności możesz podać nazwę bloku w tagu {/block}, na przykład {/block footer}. Nazwa musi jednak zgadzać się z nazwą bloku. W większych szablonach ta technika pomoże ci ustalić, które tagi bloku się zamykają.
  • W tym samym szablonie nie możesz bezpośrednio zdefiniować więcej tagów bloków o tej samej nazwie. Można to jednak osiągnąć za pomocą dynamicznych nazw bloków.
  • Możesz użyć n:atrybutów do definiowania bloków jak <h1 n:block=title>Welcome to my awesome homepage</h1>
  • Bloki można również używać bez nazw tylko do zastosowania filtrów{block|strip} hello {/block}

Ponowne wykorzystanie poziome {import}

Ponowne wykorzystanie poziome jest w Latte trzecim mechanizmem ponownego wykorzystania i dziedziczenia. Umożliwia ładowanie bloków z innych szablonów. Jest to podobne do sytuacji, gdy w PHP tworzymy plik z funkcjami pomocniczymi, który następnie ładujemy za pomocą require.

Chociaż dziedziczenie layoutu szablonu jest jedną z najpotężniejszych funkcji Latte, jest ograniczone do prostego dziedziczenia – szablon może rozszerzać tylko jeden inny szablon. Ponowne wykorzystanie poziome jest sposobem na osiągnięcie wielokrotnego dziedziczenia.

Miejmy plik z definicjami bloków:

{block sidebar}...{/block}

{block menu}...{/block}

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

{import 'blocks.latte'}

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

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

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

Tag {import} powinien być pierwszym tagiem szablonu po {layout}. Nazwa szablonu może być dowolnym wyrażeniem PHP:

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

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

Zawartość nadpisanych bloków można zachować, wstawiając blok tak samo, jak wstawia się 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ła 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 „szkieletem dokumentu”, który ożywiają szablony podrzędne, dziedziczenie jednostkowe pozwala tworzyć szkielety dla mniejszych jednostek treści i ponownie je wykorzystywać gdziekolwiek chcesz.

W dziedziczeniu jednostkowym kluczem jest tag {embed}. Łączy zachowanie {include} i {layout}. Umożliwia osadzenie zawartości innego szablonu lub bloku i opcjonalnie przekazanie zmiennych, tak jak w przypadku {include}. Umożliwia również nadpisanie dowolnego bloku zdefiniowanego wewnątrz osadzonego szablonu, tak jak przy użyciu {layout}.

Na przykład użyjemy elementu akordeon. Spójrzmy 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 podrzędne. Tak, jak w przypadku szablonu nadrzędnego w dziedziczeniu layoutu. Widzisz również zmienną $modifierClass.

Użyjmy naszego elementu w szablonie. Tutaj do gry wchodzi {embed}. Jest to niezwykle potężny tag, który pozwala nam robić wszystko: osadzić zawartość szablonu elementu, dodać do niego zmienne i dodać do niego bloki z własnym HTML:

{embed 'collapsible.latte', modifierClass: my-style}
	{block title}
		Witaj Świecie
	{/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">
		Witaj Świecie
	</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 osadzonych tagów tworzą oddzielną warstwę niezależną od innych bloków. Dlatego mogą mieć taką samą nazwę jak blok poza osadzeniem i nie są w żaden sposób przez niego wpływane. Za pomocą tagu include wewnątrz tagów {embed} możesz wstawić bloki tutaj utworzone, bloki z osadzonego szablonu (które nie są lokalne) oraz bloki z głównego szablonu, które z kolei lokalne. Możesz również importować 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 osadzonym szablonie *}
		{include aBlockDefinedInImportedTemplate} {* działa *}
		{include outer} {* nie działa! - blok jest w warstwie zewnętrznej *}
	{/block}
{/embed}

Osadzone szablony nie mają dostępu do zmiennych aktywnego kontekstu, ale mają dostęp do zmiennych globalnych.

Za pomocą {embed} można osadzać nie tylko szablony, ale także inne bloki, a zatem poprzedni przykład można by zapisać w ten sposób:

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


{embed collapsible, modifierClass: my-style}
	{block title}
		Witaj Świecie
	{/block}
	...
{/embed}

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

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

Przypadki użycia

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

{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: Rozszerzenie layout.latte wewnątrz 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: 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órym przekazujemy 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.0