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}© 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">
© 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:
- Create un template
layout.latteche contiene l'aspetto generale del vostro sito. - Create un template
layout-NOMESEZIONE.latteper ogni sezione del sito. Per esempiolayout-news.latte,layout-blog.lattee così via. Tutti questi template estendonolayout.lattee contengono stili e design specifici della sezione. - 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é:
- sono racchiuse nei tag
{define} - vengono disegnate solo quando le si inserisce con
{include} - 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}