Ereditarietà e riusabilità dei template

La riusabilità dei template e i meccanismi di ereditarietà sono qui per aumentare la produttività, perché ogni template contiene solo il suo contenuto unico e gli elementi e le strutture ripetuti vengono riutilizzati. Introduciamo tre concetti: ereditarietà del layout, riutilizzo orizzontale ed ereditarietà delle unità.

Il concetto di ereditarietà dei template di Latte è simile all'ereditarietà delle classi di PHP. Si definisce un modello padre da cui altri modelli figli possono estendere e sovrascrivere parti del modello padre. Funziona bene quando gli elementi condividono una struttura comune. Sembra complicato? Non preoccupatevi, non lo è.

Ereditarietà del layout {layout}

Vediamo l'ereditarietà dei modelli di layout partendo da un esempio. Si tratta di un modello padre, che chiameremo per esempio layout.latte, che definisce uno scheletro di documento 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>

Il tag {block} definisce tre blocchi che i template figli possono riempire. Il tag block serve solo a dire al motore del template che un template figlio può sovrascrivere quelle porzioni del template definendo il proprio blocco con lo stesso nome.

Un template figlio potrebbe avere questo aspetto:

{layout 'layout.latte'}

{block title}My amazing blog{/block}

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

Il tag {layout} è la chiave. Indica al motore dei template che questo template “estende” un altro template. Quando Latte esegue il rendering di questo modello, per prima cosa individua il genitore, in questo caso layout.latte.

A quel punto, il motore del template noterà i tre tag di blocco in layout.latte e sostituirà quei blocchi con il contenuto del template figlio. Si noti che, poiché il template figlio non ha definito il blocco footer, viene utilizzato il contenuto del template padre. Il contenuto all'interno di un tag {block} in un template padre viene sempre usato come fallback.

L'output potrebbe essere simile a:

<!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>

In un template figlio, i blocchi possono essere posizionati solo al livello superiore o all'interno di un altro blocco, ad esempio:

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

Inoltre, un blocco verrà sempre creato, indipendentemente dal fatto che la condizione circostante {if} sia valutata come vera o falsa. Contrariamente a quanto si potrebbe pensare, questo modello definisce un blocco.

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

Se si vuole che l'output all'interno del blocco sia visualizzato in modo condizionale, utilizzare il seguente metodo:

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

I dati al di fuori di un blocco in un modello figlio vengono eseguiti prima che il modello di layout venga reso, pertanto è possibile utilizzarlo per definire variabili come {var $foo = bar} e propagare i dati all'intera catena ereditaria:

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

...

Ereditarietà multilivello

Si possono usare tutti i livelli di ereditarietà necessari. Un modo comune di usare l'ereditarietà dei layout è il seguente approccio a tre livelli:

  1. Creare un modello layout.latte che contenga l'aspetto principale del sito.
  2. Creare un modello layout-SECTIONNAME.latte per ogni sezione del sito. Per esempio, layout-news.latte, layout-blog.latte ecc. Questi template estendono tutti layout.latte e includono stili e design specifici per ogni sezione.
  3. Creare modelli individuali per ogni tipo di pagina, come ad esempio un articolo di notizie o un articolo di blog. Questi modelli estendono il modello di sezione appropriato.

Ereditarietà dinamica del layout

È possibile usare una variabile o una qualsiasi espressione PHP come nome del template genitore, in modo che l'ereditarietà possa comportarsi dinamicamente:

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

Si può anche usare l'API Latte per scegliere automaticamente il modello di layout.

Suggerimenti

Ecco alcuni suggerimenti per lavorare con l'ereditarietà dei layout:

  • Se si usa {layout} in un modello, deve essere il primo tag del modello in quel modello.
  • Il layout può essere cercato automaticamente (come nei presentatori). In questo caso, se il modello non deve avere un layout, lo indicherà con il tag {layout none}.
  • Il tag {layout} ha l'alias {extends}.
  • Il nome del file del template esteso dipende dal caricatore di template.
  • Si possono avere tutti i blocchi che si vogliono. Si ricordi che i template figli non devono definire tutti i blocchi padre, per cui si possono inserire delle impostazioni predefinite ragionevoli in un certo numero di blocchi, per poi definire solo quelli necessari in seguito.

Blocchi {block}

Vedi anche anonimo {block}

Un blocco fornisce un modo per modificare la resa di una certa parte di un template, ma non interferisce in alcun modo con la logica che lo circonda. Prendiamo il seguente esempio per illustrare come funziona un blocco e, soprattutto, come non funziona:

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

Se si renderizza questo modello, il risultato sarebbe esattamente lo stesso con o senza i tag di blocco. I blocchi hanno accesso alle variabili degli ambiti esterni. È solo un modo per renderlo sovrascrivibile da un template figlio:

{layout 'parent.Latte'}

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

Ora, durante il rendering del template figlio, il ciclo utilizzerà il blocco definito nel template figlio child.Latte invece di quello definito in quello base parent.Latte; il template eseguito è quindi equivalente al seguente:

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

Tuttavia, se creiamo una nuova variabile all'interno di un blocco con nome o sostituiamo un valore di una variabile esistente, la modifica sarà visibile solo all'interno del blocco:

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

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

Il contenuto del blocco può essere modificato dai filtri. L'esempio seguente rimuove tutto l'HTML e lo titola:

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

Il tag può anche essere scritto come n:attribute:

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

Blocchi locali

Ogni blocco sovrascrive il contenuto del blocco padre con lo stesso nome. Ad eccezione dei blocchi locali. Sono come i metodi privati di una classe. Si può creare un modello senza preoccuparsi che, a causa della coincidenza dei nomi dei blocchi, questi vengano sovrascritti dal secondo modello.

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

Stampa dei blocchi {include}

Vedi anche {include file}

Per stampare un blocco in un punto specifico, utilizzare il tag {include blockname}:

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

<h1>{include title}</h1>

È anche possibile visualizzare i blocchi di un altro modello:

{include footer from 'main.latte'}

I blocchi stampati non hanno accesso alle variabili del contesto attivo, tranne se il blocco è definito nello stesso file in cui è incluso. Tuttavia, hanno accesso alle variabili globali.

È possibile passare le variabili al blocco nel modo seguente:

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

Si può usare una variabile o qualsiasi espressione in PHP come nome del blocco. In questo caso, aggiungere la parola chiave block prima della variabile, in modo che sia noto a tempo di compilazione che si tratta di un blocco e non di un modello di inserimento, il cui nome potrebbe anche essere nella variabile:

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

Il blocco può anche essere stampato all'interno di se stesso, il che è utile, ad esempio, quando si rende una struttura ad albero:

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

Invece di {include menu, ...} si può scrivere {include this, ...}, dove this indica il blocco corrente.

Il contenuto stampato può essere modificato da filtri. L'esempio seguente rimuove tutto l'HTML e lo titola:

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

Blocco genitore

Se è necessario stampare il contenuto del blocco dal modello genitore, l'istruzione {include parent} è sufficiente. È utile se si vuole aggiungere qualcosa al contenuto di un blocco genitore, invece di sovrascriverlo completamente.

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

Definizioni {define}

Oltre ai blocchi, in Latte esistono anche le “definizioni”. Sono paragonabili alle funzioni dei normali linguaggi di programmazione. Sono utili per riutilizzare frammenti di modelli e non ripetersi.

Latte cerca di mantenere le cose semplici, quindi fondamentalmente le definizioni sono uguali ai blocchi e tutto ciò che viene detto sui blocchi si applica anche alle definizioni. Si differenziano dai blocchi per il fatto che:

  1. sono racchiuse in tag {define}
  2. vengono resi solo quando vengono inseriti tramite {include}
  3. si possono definire dei parametri per esse, come le funzioni in 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> *}

Immaginate di avere un modello di aiuto con una raccolta di definizioni su come disegnare i moduli 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}

Gli argomenti di una definizione sono sempre opzionali con valore predefinito null, a meno che non sia specificato un valore predefinito (qui 'text' è il valore predefinito per $type). I tipi di parametri possono anche essere dichiarati: {define input, string $name, ...}.

Il template con le definizioni viene caricato utilizzando {import}. Le definizioni stesse sono rese allo stesso modo dei blocchi:

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

Le definizioni non hanno accesso alle variabili del contesto attivo, ma hanno accesso alle variabili globali.

Nomi dinamici dei blocchi

Latte consente una grande flessibilità nella definizione dei blocchi, perché il nome del blocco può essere una qualsiasi espressione PHP. Questo esempio definisce tre blocchi denominati hi-Peter, hi-John e hi-Mary:

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

Per esempio, possiamo ridefinire solo un blocco in un modello figlio:

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

Quindi l'output sarà simile a questo:

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

Verifica dell'esistenza del blocco {ifset}

Vedi anche {ifset $var}

Utilizzare il test {ifset blockname} per verificare se un blocco (o più blocchi) esiste nel contesto corrente:

{ifset footer}
	...
{/ifset}

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

Si può usare una variabile o qualsiasi espressione in PHP come nome del blocco. In questo caso, aggiungere la parola chiave block prima della variabile, per chiarire che non è la variabile a essere controllata:

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

L'esistenza di blocchi viene restituita anche dalla funzione hasBlock():

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

Suggerimenti

Ecco alcuni suggerimenti per lavorare con i blocchi:

  • L'ultimo blocco di primo livello non deve avere il tag di chiusura (il blocco termina con la fine del documento). Questo semplifica la scrittura di modelli figli, che hanno un unico blocco primario.
  • Per una maggiore leggibilità, si può opzionalmente dare un nome al tag {/block}, per esempio {/block footer}. Tuttavia, il nome deve corrispondere al nome del blocco. Nei template più grandi, questa tecnica aiuta a vedere quali tag di blocco vengono chiusi.
  • Non è possibile definire direttamente più tag di blocco con lo stesso nome nello stesso template. Tuttavia, si può ottenere questo risultato usando nomi di blocco dinamici.
  • Si possono usare n:attributi per definire blocchi come <h1 n:block=title>Welcome to my awesome homepage</h1>
  • I blocchi possono anche essere usati senza nome solo per applicare i filtri all'output: {block|strip} hello {/block}

Riutilizzo orizzontale {import}

Il riutilizzo orizzontale è un terzo meccanismo di riusabilità ed ereditarietà di Latte. Permette di caricare blocchi da altri modelli. È simile alla creazione di un file PHP con funzioni di aiuto o un tratto.

L'ereditarietà dei layout dei modelli è una delle caratteristiche più potenti di Latte, ma è limitata all'ereditarietà semplice: un modello può estendere solo un altro modello. Il riutilizzo orizzontale è un modo per ottenere l'ereditarietà multipla.

Prendiamo un insieme di definizioni di blocchi:

{block sidebar}...{/block}

{block menu}...{/block}

Utilizzando il comando {import}, importare tutti i blocchi e le definizioni definite in blocks.latte in un altro modello:

{import 'blocks.latte'}

{* I blocchi sidebar e menu possono ora essere utilizzati *}

Se si importano i blocchi nel template padre (cioè si usa {import} in layout.latte), i blocchi saranno disponibili anche in tutti i template figli, il che è molto utile.

Il modello che si intende importare (ad esempio blocks.latte) non deve estendere un altro modello, cioè usare {layout}. Tuttavia, può importare altri template.

Il tag {import} deve essere il primo tag del template dopo {layout}. Il nome del template può essere una qualsiasi espressione PHP:

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

Si possono usare tutte le dichiarazioni {import} che si vogliono in un dato template. Se due template importati definiscono lo stesso blocco, vince il primo. Tuttavia, la massima priorità è data al modello principale, che può sovrascrivere qualsiasi blocco importato.

Tutti i blocchi sovrascritti possono essere inclusi gradualmente inserendoli come blocco padre:

{layout 'layout.latte'}

{import 'blocks.latte'}

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

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

In questo esempio, {include parent} richiamerà correttamente il blocco sidebar dal modello blocks.latte.

Ereditarietà delle unità {embed}

L'ereditarietà delle unità porta l'idea dell'ereditarietà del layout al livello dei frammenti di contenuto. Mentre l'ereditarietà del layout funziona con gli “scheletri del documento”, che vengono portati in vita da modelli figli, l'ereditarietà delle unità consente di creare scheletri per unità di contenuto più piccole e di riutilizzarle dove si vuole.

Nell'ereditarietà delle unità il tag {embed} è la chiave. Combina il comportamento di {include} e {layout}. Permette di includere il contenuto di un altro template o blocco e di passare opzionalmente delle variabili, proprio come fa {include}. Permette anche di sovrascrivere qualsiasi blocco definito all'interno del template incluso, come fa {layout}.

Per esempio, utilizzeremo l'elemento fisarmonica pieghevole. Diamo un'occhiata allo scheletro dell'elemento nel template collapsible.latte:

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

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

Il tag {block} definisce due blocchi che i template figli possono riempire. Sì, come nel caso del template genitore nel template di ereditarietà del layout. Si veda anche la variabile $modifierClass.

Utilizziamo il nostro elemento nel template. Qui entra in gioco {embed}. È un kit potentissimo che ci permette di fare tutto: includere il contenuto del template dell'elemento, aggiungere variabili e aggiungere blocchi con HTML personalizzato:

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

L'output potrebbe assomigliare a:

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

I blocchi all'interno dei tag embed formano un livello separato, indipendente dagli altri blocchi. Pertanto, possono avere lo stesso nome del blocco fuori dal tag embed e non sono influenzati in alcun modo. Usando il tag include all'interno dei tag {embed} si possono inserire blocchi creati qui, blocchi dal template incorporato (che non sono locali) e anche blocchi dal template principale che sono locali. Si possono anche importare blocchi da altri file:

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

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

	{block inner}…{/block}

	{block title}
		{include inner} {* funziona, il blocco è definito all'interno dell'embed *}
		{include hello} {* funziona, il blocco è locale in questo template *}
		{include content} {* funziona, il blocco è definito nel template incorporato *}
		{include aBlockDefinedInImportedTemplate} {* funziona *}
		{include outer} {* non funziona! - il blocco è nel livello esterno *}
	{/block}
{/embed}

I template incorporati non hanno accesso alle variabili del contesto attivo, ma hanno accesso alle variabili globali.

Con {embed} è possibile inserire non solo modelli, ma anche altri blocchi, per cui l'esempio precedente potrebbe essere scritto in questo modo:

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

Se si passa un'espressione a {embed} e non è chiaro se si tratta di un blocco o di un nome di file, si aggiunge la parola chiave block o file:

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

Casi d'uso

In Latte esistono vari tipi di ereditarietà e di riutilizzo del codice. Riassumiamo i concetti principali per una maggiore chiarezza:

{include template}

Caso d'uso: Utilizzo di header.latte e footer.latte all'interno di 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}

Caso d'uso: Estensione di layout.latte all'interno di homepage.latte e 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}

Caso d'uso: sidebar.latte in single.product.latte e 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}

Caso d'uso: Una funzione che ottiene alcune variabili e produce un markup.

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}

Caso d'uso: Incorporare pagination.latte in product.table.latte e 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}
versione: 3.0