Ereditarietà e riusabilità dei template

I meccanismi di riusabilità ed ereditarietà dei template esistono per aumentare la vostra produttività: ogni template contiene solo il proprio contenuto specifico, mentre gli elementi e le strutture ricorrenti vengono riutilizzati. Presentiamo tre concetti: Ereditarietà del layout, Riuso orizzontale e Ereditarietà delle unità.

Il concetto di ereditarietà dei template in Latte è simile all'ereditarietà delle classi in PHP. Definite un template genitore dal quale altri template figli possono ereditare e del quale possono sovrascrivere alcune parti. Funziona benissimo quando gli elementi condividono una struttura comune. Sembra complicato? Niente paura, è molto semplice.

Ereditarietà del layout {layout}

Guardiamo l'ereditarietà del template di layout con un esempio. Questo è un template genitore, chiamiamolo layout.latte, che definisce lo scheletro di un 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>

I tag {block} definiscono tre blocchi che i template figli possono riempire. Tutto ciò che il tag block fa è dire al motore dei template che un template figlio può sovrascrivere queste porzioni definendo un proprio blocco con lo stesso nome.

Un template figlio potrebbe avere questo aspetto:

{layout 'layout.latte'}

{block title}Il mio fantastico blog{/block}

{block content}
	<p>Benvenuti nella mia splendida home page.</p>
{/block}

Qui la chiave è il tag {layout}. Dice a Latte che questo template “estende” un altro template. Quando Latte fa il rendering di questo template, individua prima il template genitore, in questo caso layout.latte.

A questo punto Latte nota i tre tag block in layout.latte e li sostituisce con il contenuto del template figlio. Poiché il template figlio non ha definito il blocco footer, viene usato il contenuto del template genitore. Il contenuto racchiuso in un tag {block} nel template genitore funge sempre da ripiego.

L'output potrebbe avere questo aspetto:

<!doctype html>
<html lang="en">
<head>
	<title>Il mio fantastico blog</title>
	<link rel="stylesheet" href="style.css">
</head>
<body>
	<div id="content">
		<p>Benvenuti nella mia splendida home page.</p>
	</div>
	<div id="footer">
		&copy; Copyright 2008
	</div>
</body>
</html>

In un template figlio i blocchi si collocano di solito al livello principale oppure dentro un altro blocco, per esempio:

{block content}
	<h1>{block title}Benvenuti nella mia splendida home page{/block}</h1>
{/block}

Inoltre un blocco viene sempre creato, indipendentemente dal fatto che la condizione {if} circostante risulti vera o falsa. Anche se non sembra, questo template definisce quindi il blocco:

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

Se volete che l'output dentro il blocco venga mostrato in modo condizionale, usate invece questa forma:

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

Il codice nell'intestazione del template figlio (cioè prima del primo blocco o di qualsiasi output) viene eseguito prima del rendering del template di layout, quindi potete usarlo per definire variabili come {var $foo = bar} e propagare i dati lungo tutta la catena di ereditarietà. Il codice collocato tra i blocchi o dopo di essi in un template con {layout} non viene eseguito affatto:

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

...

Se volete passare delle variabili solo al layout, senza crearle nel template corrente, elencatele direttamente nel tag {layout} (o {extends}) dopo una virgola:

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

La variabile $robots sarà disponibile nel layout e nei suoi blocchi, ma non nei blocchi del template corrente. Una variabile passata esplicitamente ha inoltre la precedenza su un parametro del template con lo stesso nome.

Ereditarietà su più livelli

Potete usare tutti i livelli di ereditarietà che vi servono. Un modo diffuso di usare l'ereditarietà del layout è il seguente approccio a tre livelli:

  1. Create un template layout.latte che contiene l'aspetto generale del vostro sito.
  2. Create un template layout-NOMESEZIONE.latte per ogni sezione del sito. Per esempio layout-news.latte, layout-blog.latte e così via. Tutti questi template estendono layout.latte e contengono stili e design specifici della sezione.
  3. Create singoli template per ogni tipo di pagina, per esempio un articolo di cronaca o un post del blog. Questi template estendono il template della sezione appropriata.

Ereditarietà dinamica del layout

Come nome del template genitore potete usare una variabile o qualsiasi espressione PHP, così l'ereditarietà può comportarsi in modo dinamico:

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

Potete anche usare l'API di Latte per scegliere il template di layout automaticamente.

Consigli

Ecco alcuni consigli per lavorare con l'ereditarietà del layout:

  • Se usate {layout} in un template, deve trovarsi nell'intestazione del template, cioè prima di qualsiasi output. Possono precederlo solo i tag che non producono output (come {var}, {templateType}, {import} o i commenti).
  • Il layout può essere individuato automaticamente (come nei presenter). In tal caso, se il template non deve avere un layout, lo segnala con il tag {layout none}. Al contrario, {layout auto} (o {extends auto}) ripristina la ricerca automatica del layout.
  • Il tag {layout} ha un alias: {extends}.
  • Il nome del file di layout dipende dal loader.
  • Potete avere tutti i blocchi che volete. Ricordate che i template figli non devono definire tutti i blocchi del genitore, quindi potete riempire di valori ragionevoli diversi blocchi e definire poi solo quelli che vi servono.

Blocchi {block}

Vedi anche il {block} anonimo

Un blocco offre un modo di cambiare come viene disegnata una certa parte del template, ma non interferisce in alcun modo con la logica che lo circonda. Illustriamo con l'esempio seguente come funziona un blocco e, cosa più importante, come non funziona:

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

Se fate il rendering di questo template, il risultato sarà esattamente lo stesso con o senza i tag {block}. I blocchi hanno accesso alle variabili degli ambiti esterni. Si limitano a offrire un modo per essere sovrascritti da un template figlio:

{layout 'parent.latte'}

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

Ora, facendo il rendering del template figlio, il ciclo userà il blocco definito nel template figlio child.latte invece di quello definito in parent.latte; il template eseguito equivale quindi a questo:

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

Se però creiamo una nuova variabile dentro un blocco con nome, oppure sostituiamo il valore di una esistente, la modifica sarà visibile solo dentro il blocco:

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

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

Il contenuto di un blocco si può modificare con i filtri. L'esempio seguente rimuove tutto l'HTML e converte in maiuscolo:

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

Il tag si può scrivere anche come n:attributo:

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

Blocchi locali

Ogni blocco sovrascrive il contenuto del blocco genitore con lo stesso nome, tranne i blocchi locali. Sono l'analogo dei metodi privati nelle classi. Potete creare un template senza temere che, per una coincidenza di nomi, i suoi blocchi vengano sovrascritti da un altro template.

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

Stampare i blocchi {include}

Vedi anche {include file}

Per stampare un blocco in un punto preciso, usate il tag {include blockname}:

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

<h1>{include title}</h1>

Potete anche stampare un blocco proveniente da un altro template:

{include footer from 'main.latte'}

Il blocco disegnato non ha accesso alle variabili del contesto attivo, a meno che non sia definito nello stesso file in cui viene incluso. Ha però accesso alle variabili globali.

Al blocco potete passare delle variabili così:

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

Il nome del blocco può essere una variabile o qualsiasi espressione PHP. In tal caso aggiungete la parola chiave block prima della variabile, così che Latte sappia in fase di compilazione che si tratta di un blocco e non di un template incluso, il cui nome potrebbe anch'esso trovarsi in una variabile:

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

Un blocco si può disegnare anche dentro sé stesso, il che è utile per esempio quando si disegna 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, ...} possiamo scrivere anche {include this, ...}, dove this indica il blocco corrente.

Il contenuto disegnato di un blocco si può modificare con i filtri. L'esempio seguente rimuove tutto l'HTML e converte in maiuscolo:

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

Blocco genitore

Se avete bisogno di stampare il contenuto del blocco del template genitore, usate {include parent}. È utile quando volete completare il contenuto del blocco genitore invece di sovrascriverlo del tutto.

{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, Latte ha anche le “definizioni”. Nei linguaggi di programmazione comuni sarebbero paragonabili alle funzioni. Sono utili per riutilizzare frammenti di template ed evitare ripetizioni.

Latte cerca di mantenere le cose semplici, quindi in sostanza le definizioni sono uguali ai blocchi e tutto ciò che è stato detto sui blocchi vale anche per le definizioni. Si distinguono dai blocchi perché:

  1. sono racchiuse nei tag {define}
  2. vengono disegnate solo quando le si inserisce con {include}
  3. potete definirvi dei parametri, come per le funzioni in PHP
{block foo}<p>Hello</p>{/block}
{* stampa: <p>Hello</p> *}

{define bar}<p>World</p>{/define}
{* non stampa nulla *}

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

Immaginate di avere un template di supporto con una raccolta di definizioni su come disegnare i form 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 sono sempre facoltativi, con valore predefinito null, a meno che non venga indicato un valore predefinito (qui 'text' è il valore predefinito di $type). Si possono dichiarare anche i tipi dei parametri: {define input, string $name, ...}.

Il template con le definizioni si carica con {import}. Le definizioni stesse si disegnano allo stesso modo dei blocchi:

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

Come i blocchi, le definizioni non hanno accesso alle variabili del contesto attivo, ma solo alle variabili globali. Fa eccezione una definizione che non ha parametri dichiarati, viene richiamata con un nome statico ed è inclusa nello stesso file in cui è definita: una definizione del genere ha accesso alle variabili di contesto del punto da cui viene inclusa.

Nomi dinamici dei blocchi

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

{foreach [Peter, John, Mary] as $name}
	{block "hi-$name"}Ciao, sono {$name}.{/block}
{/foreach}

Nel template figlio possiamo poi ridefinire, per esempio, un solo blocco:

{block hi-John}Salve. Sono {$name}.{/block}

L'output avrà quindi questo aspetto:

Ciao, sono Peter.
Salve. Sono John.
Ciao, sono Mary.

Verificare l'esistenza di un blocco {ifset}

Vedi anche {ifset $var}

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

{ifset footer}
	...
{/ifset}

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

Il nome del blocco può essere una variabile o qualsiasi espressione PHP. In tal caso aggiungete la parola chiave block prima della variabile, per chiarire che non si tratta di una verifica dell'esistenza di variabili:

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

L'esistenza dei blocchi si verifica anche con la funzione hasBlock():

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

Consigli

Alcuni consigli per lavorare con i blocchi:

  • L'ultimo blocco di primo livello non ha bisogno del tag di chiusura (il blocco termina con la fine del documento). Questo semplifica la scrittura dei template figli che contengono un unico blocco principale.
  • Per una migliore leggibilità potete facoltativamente indicare il nome del blocco anche nel tag {/block}, per esempio {/block footer}. Il nome deve però corrispondere a quello del blocco. Nei template più grandi questa tecnica aiuta a vedere quali tag block si stanno chiudendo.
  • Non potete definire direttamente più tag block con lo stesso nome nello stesso template. Lo si può però ottenere con i nomi dinamici dei blocchi.
  • Potete usare gli n:attributi per definire i blocchi, per esempio <h1 n:block=title>Benvenuti nella mia splendida home page</h1>
  • I blocchi si possono usare anche senza nome, solo per applicare dei filtri all'output: {block|strip} hello {/block}

Riuso orizzontale {import}

Il riuso orizzontale è il terzo meccanismo di riuso ed ereditarietà di Latte. Permette di caricare blocchi da altri template. È simile a creare in PHP un file con funzioni di supporto e caricarlo poi con require.

L'ereditarietà del layout dei template è una delle funzionalità più potenti di Latte, ma è limitata all'ereditarietà semplice: un template può estendere solo un altro template. Il riuso orizzontale è un modo di ottenere l'ereditarietà multipla.

Prendiamo un file con definizioni di blocchi:

{block sidebar}...{/block}

{block menu}...{/block}

Con il comando {import} importiamo in un altro template tutti i blocchi e tutte le definizioni definiti in blocks.latte:

{import 'blocks.latte'}

{* ora si possono usare i blocchi sidebar e menu *}

Se importate i blocchi nel template genitore (cioè usate {import} in layout.latte), i blocchi saranno disponibili anche in tutti i template figli, il che è molto pratico.

Il template destinato all'importazione (per esempio blocks.latte) non deve estendere un altro template, cioè non deve usare {layout}. Può però importare altri template.

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

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

In un template potete usare tutte le istruzioni {import} che volete. Se due template importati definiscono lo stesso blocco, vince il primo. Il template principale ha però la priorità più alta e può sovrascrivere qualsiasi blocco importato.

Il tag {import} può anche passare argomenti al template importato, per esempio {import 'blocks.latte', foo: 1}. Questi argomenti sono poi disponibili come variabili nei blocchi e nelle definizioni importati.

Il contenuto dei blocchi sovrascritti si può conservare inserendo il blocco come si fa con il blocco genitore:

{layout 'layout.latte'}

{import 'blocks.latte'}

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

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

In questo esempio {include parent} richiama il blocco sidebar del template blocks.latte.

Ereditarietà delle unità {embed}

L'ereditarietà delle unità estende l'idea dell'ereditarietà del layout al livello dei frammenti di contenuto. Mentre l'ereditarietà del layout lavora con “scheletri di documento” che i template figli riempiono di vita, l'ereditarietà delle unità permette di creare scheletri per unità di contenuto più piccole e di riutilizzarli dove volete.

Nell'ereditarietà delle unità la chiave è il tag {embed}. Unisce il comportamento di {include} e di {layout}. Permette di incorporare il contenuto di un altro template o blocco e, facoltativamente, di passargli delle variabili, proprio come {include}. Permette inoltre di sovrascrivere qualsiasi blocco definito all'interno del template incorporato, come {layout}.

Usiamo per esempio un elemento a fisarmonica. Date un'occhiata allo scheletro dell'elemento, salvato 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>

I tag {block} definiscono due blocchi che i template figli possono riempire. Sì, esattamente come nel caso del template genitore nell'ereditarietà del layout. Vedete anche la variabile $modifierClass.

Usiamo il nostro elemento in un template. È qui che entra in gioco {embed}. È un tag estremamente potente, che ci permette di fare tutte queste cose: incorporare il contenuto del template dell'elemento, aggiungervi variabili e aggiungervi 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 avere questo aspetto:

<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 dentro i tag embed formano uno strato separato, isolato dai blocchi esterni all'embed. Possono quindi avere lo stesso nome di un blocco esterno senza entrare in conflitto, e non ne sono influenzati. Con il tag include dentro i tag {embed} potete inserire i blocchi creati lì dentro, i blocchi del template incorporato (che non siano locali) e anche i blocchi del template principale che siano locali. Potete 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 dentro l'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 è nello strato esterno *}
	{/block}
{/embed}

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

Con {embed} potete incorporare non solo template, ma anche altri blocchi, quindi l'esempio precedente si potrebbe scrivere così:

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

C'è però una differenza tra i due casi: quando incorporate un blocco anziché un file, i blocchi dello strato esterno restano accessibili dentro l'embed. A differenza di quanto accade con un file incorporato, lì {include outer} funzionerebbe.

Se passiamo a {embed} un'espressione e non è chiaro se sia il nome di un blocco o di un file, aggiungete la parola chiave block o file:

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

Casi d'uso

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

{include template}

Caso d'uso: usare header.latte e footer.latte dentro 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: estendere layout.latte dentro 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: usare 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: funzioni che ricevono variabili e disegnano qualcosa.

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