Ereditarietà e riutilizzo dei template

I meccanismi di riutilizzo e di ereditarietà dei template aumenteranno la vostra produttività, poiché 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 e ereditarietà unitaria.

Il concetto di ereditarietà dei template di Latte è simile all'ereditarietà delle classi in PHP. Si definisce un template genitore da cui altri template figli possono ereditare e sovrascrivere parti del template genitore. Funziona alla grande quando gli elementi condividono una struttura comune. Sembra complicato? Non preoccupatevi, è molto semplice.

Ereditarietà del layout {layout}

Vediamo l'ereditarietà del template di layout, cioè del layout, con un esempio. Questo è un template genitore, che chiameremo ad esempio layout.latte, e 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. Il tag block fa solo questo: annuncia che questo posto può essere sovrascritto da un template figlio definendo il proprio blocco con lo stesso nome.

Un template figlio può assomigliare a questo:

{layout 'layout.latte'}

{block title}My amazing blog{/block}

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

La chiave qui è il tag {layout}. Dice a Latte che questo template “estende” un altro template. Quando Latte renderizza questo template, prima trova il template genitore – in questo caso layout.latte.

A questo punto, Latte nota i tre tag block in layout.latte e sostituisce questi blocchi con il contenuto del template figlio. Poiché il template figlio non ha definito un blocco footer, viene utilizzato invece il contenuto del template genitore. Il contenuto all'interno del tag {block} nel template genitore viene sempre utilizzato come fallback.

L'output potrebbe assomigliare a questo:

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

Nel template figlio, i blocchi possono essere posizionati solo al livello più alto o all'interno di un altro blocco, cioè:

{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 {if} circostante sia valutata come vera o falsa. Quindi, anche se non sembra, questo template definisce il blocco.

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

Se si desidera che l'output all'interno del blocco venga visualizzato condizionatamente, utilizzare invece quanto segue:

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

Lo spazio al di fuori dei blocchi nel template figlio viene eseguito prima del rendering del template di layout, quindi è possibile utilizzarlo per definire variabili come {var $foo = bar} e per propagare i dati lungo l'intera catena di ereditarietà:

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

...

Ereditarietà multilivello

È possibile utilizzare tutti i livelli di ereditarietà necessari. Un modo comune per utilizzare l'ereditarietà del layout è il seguente approccio a tre livelli:

  1. Creare un template layout.latte che contenga lo scheletro principale dell'aspetto del sito.
  2. Creare un template layout-SECTIONNAME.latte per ogni sezione del sito. Ad esempio layout-news.latte, layout-blog.latte, ecc. Tutti questi template estendono layout.latte e includono stili e design specifici per le singole sezioni.
  3. Creare template individuali per ogni tipo di pagina, ad esempio un articolo di giornale o un post di blog. Questi template estendono il template della sezione corrispondente.

Ereditarietà dinamica

Come nome del template genitore si può usare una variabile o qualsiasi espressione PHP, quindi l'ereditarietà può comportarsi dinamicamente:

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

È anche possibile utilizzare l'API di Latte per selezionare automaticamente il template di layout.

Suggerimenti

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

  • Se si utilizza {layout} in un template, deve essere il primo tag del template in quel template.
  • Il layout può essere trovato automaticamente (come ad esempio nei presenter). In tal caso, se il template non deve avere un layout, lo annuncia con il tag {layout none}.
  • Il tag {layout} ha un alias {extends}.
  • Il nome del file di layout dipende dal loader.
  • È possibile avere quanti blocchi si desidera. Ricordate che i template figli non devono definire tutti i blocchi genitori, quindi potete riempire valori predefiniti ragionevoli in alcuni blocchi e poi definire solo quelli necessari in seguito.

Blocchi {block}

Vedi anche {block} anonimo

Un blocco rappresenta un modo per modificare come viene renderizzata una certa parte del template, ma non interferisce in alcun modo con la logica circostante. Nell'esempio seguente, mostreremo come funziona un blocco, ma anche come non funziona:

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

Se si renderizza questo template, il risultato sarà esattamente lo stesso con o senza i tag {block}. I blocchi hanno accesso alle variabili dagli ambiti esterni. Danno solo la possibilità di essere sovrascritti 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 del blocco definito in 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 nominato o sostituiamo il valore di una 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 utilizzando i filtri. L'esempio seguente rimuove tutto l'HTML e cambia le maiuscole/minuscole:

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

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

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

Blocchi locali

Ogni blocco sovrascrive il contenuto del blocco genitore con lo stesso nome – ad eccezione dei blocchi locali. Nelle classi, sarebbero qualcosa come metodi privati. In questo modo è possibile creare un template senza preoccuparsi che, a causa della corrispondenza dei nomi dei blocchi, vengano sovrascritti da un altro template.

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

Rendering dei blocchi {include}

Vedi anche {include file}

Per visualizzare un blocco in una posizione specifica, utilizzare il tag {include blockname}:

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

<h1>{include title}</h1>

È anche possibile visualizzare un blocco da un altro template:

{include footer from 'main.latte'}

Il blocco renderizzato non ha accesso alle variabili del contesto attivo, tranne nel caso in cui il blocco sia definito nello stesso file in cui è incluso. Tuttavia, ha accesso alle variabili globali.

È possibile passare variabili al blocco in questo modo:

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

Come nome del blocco si può usare una variabile o qualsiasi espressione PHP. In tal caso, aggiungiamo la parola chiave block prima della variabile, in modo che Latte sappia già in fase di compilazione che si tratta di un blocco e non di inclusione del template, il cui nome potrebbe anche essere in una variabile:

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

Un blocco può essere renderizzato anche all'interno di sé stesso, il che è utile ad esempio per renderizzare 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 quindi scrivere {include this, ...}, dove this significa il blocco corrente.

Il blocco renderizzato può essere modificato utilizzando i filtri. L'esempio seguente rimuove tutto l'HTML e cambia le maiuscole/minuscole:

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

Blocco genitore

Se è necessario visualizzare il contenuto di un blocco dal template genitore, utilizzare {include parent}. Questo è utile se si desidera solo aggiungere contenuto al 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”. Nei linguaggi di programmazione comuni, le paragoneremmo alle funzioni. Sono utili per riutilizzare frammenti di template per non ripetersi.

Latte cerca di rendere le cose semplici, quindi fondamentalmente le definizioni sono uguali ai blocchi e tutto ciò che viene detto sui blocchi vale anche per le definizioni. Si differenziano dai blocchi in quanto:

  1. sono racchiuse nei tag {define}
  2. vengono renderizzate solo quando le si include tramite {include}
  3. è possibile definire parametri per loro, in modo simile alle 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 template di aiuto con una raccolta di definizioni su come disegnare 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 opzionali con un valore predefinito null, a meno che non venga specificato un valore predefinito (qui 'text' è il valore predefinito per $type). È possibile dichiarare anche i tipi dei parametri: {define input, string $name, ...}.

Carichiamo il template con le definizioni utilizzando {import}. Le definizioni stesse vengono renderizzate 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, poiché il nome del blocco può essere qualsiasi espressione PHP. Questo esempio definisce tre blocchi con i nomi hi-Peter, hi-John e hi-Mary:

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

Nel template figlio possiamo quindi ridefinire, ad esempio, solo un blocco:

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

Quindi l'output assomiglierà a questo:

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

Controllo dell'esistenza dei blocchi {ifset}

Vedi anche {ifset $var}

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

{ifset footer}
	...
{/ifset}

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

Come nome del blocco si può usare una variabile o qualsiasi espressione PHP. In tal caso, aggiungiamo la parola chiave block prima della variabile per chiarire che non si tratta di un test sull'esistenza di variabili:

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

L'esistenza dei blocchi viene verificata anche dalla funzione hasBlock():

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

Suggerimenti

Alcuni suggerimenti per lavorare con i blocchi:

  • L'ultimo blocco di livello superiore non deve avere un tag di chiusura (il blocco termina alla fine del documento). Questo semplifica la scrittura di template figli che contengono un blocco primario.
  • Per una migliore leggibilità, è possibile specificare il nome del blocco nel tag {/block}, ad esempio {/block footer}. Tuttavia, il nome deve corrispondere al nome del blocco. Nei template più grandi, questa tecnica vi aiuterà a vedere quali tag di blocco vengono chiusi.
  • Non è possibile definire direttamente più tag di blocco con lo stesso nome nello stesso template. Tuttavia, questo può essere ottenuto utilizzando nomi dinamici dei blocchi.
  • È possibile utilizzare n:attributi per definire blocchi come <h1 n:block=title>Welcome to my awesome homepage</h1>
  • I blocchi possono essere utilizzati anche senza nomi solo per applicare filtri{block|strip} hello {/block}

Riutilizzo orizzontale {import}

Il riutilizzo orizzontale è il terzo meccanismo in Latte per il riutilizzo e l'ereditarietà. Consente di caricare blocchi da altri template. È simile a quando in PHP creiamo un file con funzioni di aiuto che poi carichiamo usando require.

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

Abbiamo un file con definizioni di blocchi:

{block sidebar}...{/block}

{block menu}...{/block}

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

{import 'blocks.latte'}

{* ora è possibile utilizzare i blocchi sidebar e menu *}

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

Il template destinato all'importazione (ad es. blocks.latte) non deve estendere un altro template, cioè usare {layout}. Tuttavia, può 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'}

È possibile utilizzare quanti comandi {import} si desidera nel template. Se due template importati definiscono lo stesso blocco, vince il primo. Tuttavia, la priorità più alta spetta al template principale, che può sovrascrivere qualsiasi blocco importato.

Il contenuto dei blocchi sovrascritti può essere preservato includendo il blocco nello stesso modo in cui viene incluso 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} chiama il blocco sidebar dal template blocks.latte.

Ereditarietà unitaria {embed}

L'ereditarietà unitaria estende l'idea dell'ereditarietà del layout al livello dei frammenti di contenuto. Mentre l'ereditarietà del layout lavora con lo “scheletro del documento” che viene animato dai template figli, l'ereditarietà unitaria consente di creare scheletri per unità di contenuto più piccole e riutilizzarle ovunque si desideri.

Nell'ereditarietà unitaria, la chiave è il tag {embed}. Combina il comportamento di {include} e {layout}. Consente di incorporare il contenuto di un altro template o blocco e passare opzionalmente variabili, proprio come nel caso di {include}. Consente inoltre di sovrascrivere qualsiasi blocco definito all'interno del template incorporato, come quando si utilizza {layout}.

Ad esempio, utilizziamo un elemento accordion. Vediamo lo 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ì, come nel caso del template genitore nell'ereditarietà del layout. Vedete anche la variabile $modifierClass.

Utilizziamo il nostro elemento nel template. Qui entra in gioco {embed}. È un tag estremamente potente che ci consente di fare tutte queste cose: incorporare il contenuto del template dell'elemento, aggiungervi variabili e aggiungervi blocchi con il nostro 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 questo:

<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 incorporati formano un livello separato indipendente dagli altri blocchi. Pertanto, possono avere lo stesso nome di un blocco al di fuori dell'incorporamento e non ne sono influenzati in alcun modo. Utilizzando il tag include all'interno dei tag {embed}, è possibile includere i blocchi creati qui, i blocchi dal template incorporato (che non sono locali) e anche i blocchi dal template principale, che invece sono locali. È anche possibile 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} {* works, block is defined inside embed *}
		{include hello} {* works, block is local in this template *}
		{include content} {* works, block is defined in embedded template *}
		{include aBlockDefinedInImportedTemplate} {* works *}
		{include outer} {* does not work! - block is in outer layer *}
	{/block}
{/embed}

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

Utilizzando {embed} è possibile includere non solo template, ma anche altri blocchi, e quindi 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 passiamo un'espressione a {embed} e non è chiaro se si tratti del nome di un blocco o di un file, aggiungiamo la parola chiave block o file:

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

Casi d'uso

In Latte esistono diversi tipi di ereditarietà e 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: Funzioni a cui passiamo variabili e che renderizzano 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: Inserimento di 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