Dziedziczenie i możliwość ponownego wykorzystania szablonów

Mechanizmy ponownego wykorzystania szablonów i dziedziczenia zwiększą Twoją produktywność, ponieważ każdy szablon zawiera tylko swoją unikalną zawartość, a powtarzające się elementy i struktury zostaną ponownie wykorzystane. Wprowadzamy trzy pojęcia: dziedziczenie układu, ponowne wykorzystanie poziome oraz dziedziczenie jednostkowe.

Koncepcja dziedziczenia szablonów Latte jest podobna do dziedziczenia klas w PHP. Definiujesz szablon nadrzędny, z którego inne szablony podrzędne mogą dziedziczyć 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 układu {layout}

Przyjrzyjmy się na przykładzie dziedziczenia szablonu układu, czyli layoutu. Jest to szablon nadrzędny, który nazwiemy na przykład layout.latte, a 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>

Znaczniki {block} definiują trzy bloki, które mogą wypełnić szablony dziecięce. Znacznik block nie robi nic więcej niż oznajmia, że to miejsce może być nadpisane przez szablon dziecka poprzez zdefiniowanie własnego bloku o tej samej nazwie.

Szablon dziecka 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}. Latte mówi, że ten szablon “rozszerza” inny szablon. Kiedy Latte renderuje ten szablon, najpierw znajduje rodzica – w tym przypadku layout.latte.

W tym momencie Latte zauważa trzy znaczniki bloków w layout.latte i zastępuje te bloki zawartością szablonu dziecka. Ponieważ szablon dziecka nie zdefiniował bloku footer, zamiast niego używana jest zawartość z szablonu rodzica. Zawartość w tagu {block} w szablonie nadrzędnym jest zawsze używana jako fallback.

Dane wyjściowe mogą 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 dziecka bloki mogą być umieszczane tylko na najwyższym poziomie lub wewnątrz innego bloku, tj :

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

Ponadto blok zostanie zawsze utworzony niezależnie od tego, czy otaczający go warunek {if} oceniany jest jako prawdziwy czy fałszywy. Więc nawet jeśli nie wygląda, to 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 pojawiło się warunkowo, użyj zamiast tego następującego:

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

Rozmieść bloki w szablonie dziecka przed renderowaniem szablonu układu, abyś mógł użyć ich do zdefiniowania zmiennych, takich jak {var $foo = bar} i propagować dane do całego łańcucha dziedziczenia:

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

...

Dziedziczenie wielopoziomowe

Możesz użyć tylu poziomów dziedziczenia, ile potrzebujesz. Powszechnym sposobem korzystania z dziedziczenia układu jest następujące trzypoziomowe podejście:

  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, itd. Wszystkie te szablony rozszerzają layout.latte i zawierają style & design specyficzne dla każdej sekcji.
  3. Utwórz indywidualne szablony dla każdego typu strony, np. artykułu prasowego lub wpisu na blogu. Szablony te rozszerzają odpowiedni szablon sekcji.

Dynamiczne dziedziczenie

Zmienna lub dowolne wyrażenie PHP może być użyte jako nazwa szablonu nadrzędnego, więc dziedziczenie może zachowywać się dynamicznie:

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

Możesz również użyć interfejsu API Latte, aby automatycznie wybrać szablon układu.

Porady

Oto kilka wskazówek dotyczących pracy z dziedziczeniem układów:

  • Jeśli używasz {layout} w szablonie , musi to być pierwszy tag szablonu w tym szablonie.
  • Układ może być wyszukiwany automatycznie (jak w prezenterach). W takim przypadku, jeśli szablon nie powinien mieć układu, wskaże to za pomocą znacznika {layout none}.
  • Znacznik {layout} ma alias {extends}.
  • Nazwa pliku układu zależy od programu ładującego.
  • Możesz mieć tyle bloków, ile chcesz. Zauważ, że szablony dzieci nie muszą definiować wszystkich bloków nadrzędnych, więc możesz wypełnić rozsądne domyślne ustawienia w kilku blokach, a następnie zdefiniować tylko te, których potrzebujesz później.

Bloki {block}

Zobacz także anonimowość {block}

Blok jest sposobem na zmianę sposobu renderowania pewnej części szablonu, ale nie ingeruje w logikę wokół niego. W poniższym przykładzie pokażemy jak działa blok, ale też 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 i bez znaczników {block}. Bloki mają dostęp do zmiennych z zewnętrznych zakresów. Po prostu dają opcję do nadpisania przez szablon dziecka:

{layout 'parent.Latte'}

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

Teraz podczas renderowania szablonu dziecięcego pętla użyje bloku zdefiniowanego w szablonie dziecięcym child.Latte zamiast bloku zdefiniowanego w parent.Latte; działający szablon ma wtedy odpowiednik:

{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 = 'new value'}
	{var $bar = 'bar'}
{/block}

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

Zawartość bloku może być modyfikowana za pomocą filtrów. Poniższy przykład usuwa cały HTML i zmienia wielkość liter:

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

Znacznik może być również zapisany jako n:attribut:

<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 prywatnych metod. Możesz więc stworzyć szablon bez obaw, że z powodu zgodności nazw bloków zostaną one nadpisane z innego szablonu.

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

Rendering bloków {include}

Zobacz także. {include file}

Aby wymienić blok w konkretnym miejscu, należy użyć znacznika {include blockname}:

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

<h1>{include title}</h1>

Można również wylistować blok z innego szablonu:

{include footer from 'main.latte'}

Wyrenderowany blok nie ma dostępu do aktywnych zmiennych kontekstowych, z wyjątkiem sytuacji, gdy blok jest zdefiniowany w tym samym pliku, w którym został wstawiony. Ma jednak dostęp do zmiennych globalnych.

Do bloku można przekazać zmienne w następujący sposób:

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

Jako nazwy bloku możesz użyć zmiennej lub dowolnego wyrażenia PHP. W tym przypadku dodajemy słowo kluczowe block przed zmienną, aby w czasie kompilacji Latte wiedziało już, że chodzi o blok, a nie o wstawienie szablonu, którego nazwa również mogłaby znaleźć się w zmiennej:

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

Blok może być również renderowany wewnątrz siebie, co jest przydatne na przykład przy renderowaniu struktury drzewa:

{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ć {include this, ...}, gdzie this oznacza bieżący blok.

Wyrenderowany blok może być modyfikowany za pomocą filtrów. Poniższy przykład usuwa cały HTML i zmienia wielkość liter:

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

Blok macierzysty

Jeśli potrzebujesz zrzucić zawartość bloku z szablonu nadrzędnego, użyj {include parent}. Jest to przydatne, jeśli chcesz tylko dodać zawartość bloku nadrzędnego zamiast całkowicie go nadpisywać.

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

Definicja: {define}

Oprócz bloków, w Latte są też “definicje”. W normalnych językach programowania przyrównalibyśmy je do funkcji. Są one przydatne do ponownego wykorzystania fragmentów szablonów, aby nie powtarzać się.

Latte stara się zachować prostotę, więc zasadniczo definicje są takie same jak bloki, a wszystko, co powiedziano o blokach, dotyczy również definicji. Różnią się one od bloków tym:

  1. są zamknięte w tagach {define}
  2. są renderowane tylko wtedy, gdy są wstawiane przez {include}
  3. można definiować dla nich parametry, tak jak funkcje w PHP
{block foo}<p>Hello</p>{/block}
{* prints: <p>Hello</p> *}

{define bar}<p>World</p>{/define}
{* prints nothing *}

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

Wyobraź sobie, że masz szablon pomocniczy z kolekcją definicji dotyczących rysowania formularzy 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 definicji są zawsze opcjonalne z wartością domyślną null, chyba że określono wartość domyślną (tutaj 'text' jest wartością domyślną dla $type). Typy parametrów mogą być również zadeklarowane: {define input, string $name, ...}.

Szablon z definicjami jest ładowany przy użyciu {import}. Same definicje są renderowane w taki sam sposób jak 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. W tym przykładzie zdefiniowano trzy bloki o nazwach hi-Peter, hi-John, oraz hi-Mary:

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

Możemy wtedy przedefiniować tylko jeden blok w szablonie dziecka, np:

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

Więc wyjście będzie wyglądać tak:

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

Sprawdź, czy istnieją bloki {ifset}

Zobacz także. {ifset $var}

Użyj testu {ifset blockname}, aby sprawdzić, czy blok (lub wiele bloków) istnieje w bieżącym kontekście:

{ifset footer}
	...
{/ifset}

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

Nazwa bloku może być zmienną lub dowolnym wyrażeniem PHP. W tym przypadku dodajemy słowo kluczowe block przed zmienną, aby wyjaśnić, że nie jest to test na istnienie zmiennych:

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

Istnienie bloków jest również zwracane przez funkcję hasBlock():

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

Porady

Kilka wskazówek dotyczących pracy z klockami:

  • Ostatni blok najwyższego poziomu nie musi mieć znacznika zamykającego (blok kończy się na końcu dokumentu). Upraszcza to pisanie szablonów dzieci, które zawierają jeden blok główny.
  • Dla lepszej czytelności można zawrzeć nazwę bloku w znaczniku {/block}, na przykład {/block footer}. Nazwa ta musi jednak odpowiadać nazwie bloku. W większych szablonach ta technika pomaga określić, które znaczniki blokowe zamykają.
  • Nie można bezpośrednio zdefiniować wielu znaczników blokowych o tej samej nazwie w tym samym szablonie. Można to jednak osiągnąć poprzez zastosowanie dynamicznych nazw bloków.
  • Możesz użyć n:attributes do zdefiniowania bloków jako <h1 n:block=title>Welcome to my awesome homepage</h1>
  • Możesz również użyć bloków bez nazw tylko do zastosowania filtrów{block|strip} hello {/block}

Ponowne wykorzystanie poziome {import}

Poziome ponowne użycie jest trzecim mechanizmem wielokrotnego użycia i dziedziczenia w Latte. Pozwala na wczytywanie bloków z innych szablonów. Jest to podobne do tworzenia pliku PHP z funkcjami pomocniczymi.

Dziedziczenie układu szablonu jest jedną z najpotężniejszych funkcji Latte, ale jest ograniczone do prostego dziedziczenia – szablon może rozszerzyć tylko jeden inny szablon. Ponowne użycie horyzontalne jest sposobem na osiągnięcie wielokrotnego dziedziczenia.

Weźmy zestaw definicji bloków:

{block sidebar}...{/block}

{block menu}...{/block}

Używając polecenia {import}, zaimportuj wszystkie bloki i definicje zdefiniowane w blocks.latte do innego szablonu:

{import 'blocks.latte'}

{* bloki paska bocznego i menu mogą być teraz używane *}

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

Szablon, który ma zostać zaimportowany (np. blocks.latte) nie może rozszerzać innego szablonu, tj. używać {layout}. Może on jednak importować inne szablony.

Znacznik {import} powinien być pierwszym znacznikiem 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ć dowolnej ilości oświadczeń {import}. Jeśli dwa importowane szablony definiują ten sam blok, wygrywa pierwszy. Jednak szablon główny ma najwyższy priorytet i może zastąpić każdy zaimportowany blok.

Do wszystkich nadrzędnych bloków można uzyskać dostęp sekwencyjny, wstawiając je jako 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 jednostek {embed}

Dziedziczenie jednostek rozszerza ideę dziedziczenia układu na poziom fragmentów treści. Podczas gdy dziedziczenie układu działa z “szkieletem dokumentu”, który jest animowany przez szablony dzieci, dziedziczenie jednostek pozwala na tworzenie szkieletów dla mniejszych jednostek treści i ponowne wykorzystanie ich gdziekolwiek chcesz.

W dziedziczeniu jednostek kluczem jest znacznik {embed}. Łączy w sobie zachowanie {include} i {layout}. Pozwala na wstawienie treści innego szablonu lub bloku i opcjonalnie przekazanie zmiennych, jak w przypadku {include}. Pozwala również na nadpisanie dowolnego bloku zdefiniowanego wewnątrz wstawionego szablonu, tak jak w przypadku użycia {layout}.

Na przykład używamy elementu accordion. Przyjrzyjmy się szkieletowi elementu zapisanego 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>

Znaczniki {block} definiują dwa bloki, które szablony dziecięce mogą wypełnić. Tak, jak w przypadku szablonu nadrzędnego w dziedziczeniu układu. Można też zobaczyć zmienną $modifierClass.

Wykorzystajmy nasz element w szablonie. Tu z pomocą przychodzi {embed}. Jest to niezwykle potężny znacznik, który pozwala nam robić różne rzeczy: wstawiać zawartość elementu szablonu, dodawać do niego zmienne oraz dodawać bloki z niestandardowym 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}

Dane wyjściowe mogą 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 osadzonych znaczników tworzą osobną warstwę niezależną od pozostałych bloków. Dlatego mogą mieć taką samą nazwę jak blok poza wstawką i nie są w żaden sposób dotknięte. Używając znacznika include wewnątrz znaczników {embed}, można wstawiać bloki utworzone tutaj, bloki z szablonu osadzonego (które nie są lokalne) oraz bloki z szablonu głównego, które lokalne. Można 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 szablonie osadzonym *}
		{include aBlockDefinedInImportedTemplate} {* works *}
		{include outer} {* nie działa! - blok jest w warstwie zewnętrznej *}
	{/block}
{/embed}

Szablony wbudowane nie mają dostępu do aktywnych zmiennych kontekstowych, ale mają dostęp do zmiennych globalnych.

Nie tylko szablony, ale także inne bloki można wstawiać za pomocą {embed}, więc poprzedni przykład można było napisać 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}
		Hello World
	{/block}
	...
{/embed}

Jeśli do {embed} przekazujemy 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 rodzaje dziedziczenia i ponownego wykorzystania kodu. Dla jasności podsumujmy główne pojęcia:

{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}

Użytek: Funkcja, do której przekazujemy zmienne i renderuje coś.

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: Wstaw pagination.latte do 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