Herencia y reutilización de plantillas

La reutilización de plantillas y los mecanismos de herencia están aquí para aumentar tu productividad, ya que cada plantilla contiene sólo su contenido único y los elementos y estructuras repetidos se reutilizan. Introducimos tres conceptos: herencia de plantillas, reutilización horizontal y herencia de unidades.

El concepto de herencia de plantillas Latte es similar a la herencia de clases PHP. Se define una plantilla padre a partir de la cual otras plantillas hijas pueden extender y anular partes de la plantilla padre. Funciona muy bien cuando los elementos comparten una estructura común. ¿Suena complicado? No te preocupes, no lo es.

Herencia de diseño {layout}

Veamos la herencia de plantillas de diseño comenzando con un ejemplo. Esta es una plantilla padre que llamaremos por ejemplo layout.latte y define un documento HTML esqueleto.

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

La etiqueta {block} define tres bloques que las plantillas hijas pueden rellenar. Todo lo que hace la etiqueta block es decirle al motor de plantillas que una plantilla hija puede sobreescribir esas partes de la plantilla definiendo su propio bloque con el mismo nombre.

Una plantilla hija podría tener este aspecto:

{layout 'layout.latte'}

{block title}My amazing blog{/block}

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

La etiqueta {layout} es la clave aquí. Indica al motor de plantillas que esta plantilla “extiende” otra plantilla. Cuando Latte renderiza esta plantilla, primero localiza el padre – en este caso, layout.latte.

En ese momento, el motor de plantillas se dará cuenta de las tres etiquetas de bloque en layout.latte y reemplazará esos bloques con el contenido de la plantilla hija. Tenga en cuenta que, dado que la plantilla hija no definió el bloque footer, en su lugar se utiliza el contenido de la plantilla padre. El contenido de una etiqueta {block} en una plantilla padre se utiliza siempre como alternativa.

El resultado podría ser el siguiente:

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

En una plantilla hija, los bloques sólo pueden situarse en el nivel superior o dentro de otro bloque, es decir:

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

Además, siempre se creará un bloque independientemente de si la condición {if} que lo rodea se evalúa como verdadera o falsa. Contrariamente a lo que podría pensarse, esta plantilla sí define un bloque.

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

Si desea que la salida dentro del bloque se muestre condicionalmente, utilice lo siguiente en su lugar:

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

Los datos fuera de un bloque en una plantilla hija se ejecutan antes de que se renderice la plantilla de diseño, por lo que puede utilizarlo para definir variables como {var $foo = bar} y propagar datos a toda la cadena de herencia:

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

...

Herencia multinivel

Puede utilizar tantos niveles de herencia como necesite. Una forma común de utilizar la herencia de diseño es el siguiente enfoque de tres niveles:

  1. Cree una plantilla layout.latte que contenga el aspecto principal de su sitio.
  2. Cree una plantilla layout-SECTIONNAME.latte para cada sección de su sitio. Por ejemplo, layout-news.latte, layout-blog.latte, etc. Todas estas plantillas amplían layout.latte e incluyen estilos/diseños específicos de cada sección.
  3. Cree plantillas individuales para cada tipo de página, como un artículo de noticias o una entrada de blog. Estas plantillas amplían la plantilla de sección correspondiente.

Herencia de diseño dinámico

Puede utilizar una variable o cualquier expresión PHP como nombre de la plantilla padre, de forma que la herencia pueda comportarse dinámicamente:

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

También puede utilizar la API de Latte para elegir la plantilla de diseño de forma automática.

Consejos

Aquí tienes algunos consejos para trabajar con la herencia de diseños:

  • Si utiliza {layout} en una plantilla, debe ser la primera etiqueta de plantilla en esa plantilla.
  • La maquetación puede buscarse automáticamente (como en los presentadores). En este caso, si la plantilla no debe tener un diseño, lo indicará con la etiqueta {layout none}.
  • La etiqueta {layout} tiene el alias {extends}.
  • El nombre de archivo de la plantilla extendida depende del cargador de plantillas.
  • Puede tener tantos bloques como desee. Recuerde que las plantillas hijas no tienen por qué definir todos los bloques padres, por lo que puede rellenar valores por defecto razonables en una serie de bloques y luego definir sólo los que necesite más adelante.

Bloques {block}

Véase también anónimo {block}

Un bloque permite cambiar el modo en que se representa una parte determinada de una plantilla, pero no interfiere en modo alguno con la lógica que la rodea. Tomemos el siguiente ejemplo para ilustrar cómo funciona un bloque y, lo que es más importante, cómo no funciona:

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

Si renderiza esta plantilla, el resultado sería exactamente el mismo con o sin las etiquetas de bloque. Los bloques tienen acceso a variables de ámbitos externos. Es sólo una forma de hacerlo anulable por una plantilla hija:

{layout 'parent.Latte'}

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

Ahora, al renderizar la plantilla hija, el bucle va a utilizar el bloque definido en la plantilla hija child.Latte en lugar del definido en la plantilla base parent.Latte; la plantilla ejecutada es entonces equivalente a la siguiente:

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

Sin embargo, si creamos una nueva variable dentro de un bloque con nombre o reemplazamos un valor de una existente, el cambio será visible sólo dentro del bloque:

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

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

El contenido del bloque puede modificarse mediante filtros. El siguiente ejemplo elimina todo el HTML y lo pone en mayúsculas:

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

La etiqueta también puede escribirse como n:attribute:

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

Bloques locales

Todos los bloques anulan el contenido del bloque padre del mismo nombre. Excepto los bloques locales. Son algo así como los métodos privados de una clase. Puedes crear una plantilla sin preocuparte de que, debido a la coincidencia de los nombres de los bloques, sean sobrescritos por la segunda plantilla.

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

Impresión de bloques {include}

Véase también {include file}

Para imprimir un bloque en un lugar determinado, utilice la etiqueta {include blockname}:

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

<h1>{include title}</h1>

También puede mostrar bloques de otra plantilla:

{include footer from 'main.latte'}

Los bloques impresos no tienen acceso a las variables del contexto activo, excepto si el bloque está definido en el mismo fichero donde se incluye. Sin embargo tienen acceso a las variables globales.

Puede pasar variables al bloque de la siguiente manera:

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

Puede utilizar una variable o cualquier expresión en PHP como nombre del bloque. En este caso, añada la palabra clave block antes de la variable, para que se sepa en tiempo de compilación que se trata de un bloque, y no de insertar plantilla, cuyo nombre también podría estar en la variable:

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

El bloque también puede imprimirse dentro de sí mismo, lo que resulta útil, por ejemplo, al representar una estructura de árbol:

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

En lugar de {include menu, ...} también podemos escribir {include this, ...} donde this significa bloque actual.

El contenido impreso puede modificarse mediante filtros. El siguiente ejemplo elimina todo el HTML y lo pone en mayúsculas:

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

Bloque padre

Si necesita imprimir el contenido del bloque de la plantilla padre, la sentencia {include parent} hará el truco. Esto es útil si desea añadir algo al contenido de un bloque padre en lugar de sustituirlo por completo.

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

Definiciones {define}

Además de los bloques, en Latte también hay “definiciones”. Son comparables con las funciones en los lenguajes de programación regulares. Son útiles para reutilizar fragmentos de plantillas para no repetirse.

Latte intenta simplificar las cosas, así que básicamente las definiciones son lo mismo que los bloques, y todo lo que se dice sobre los bloques también se aplica a las definiciones. Se diferencian de los bloques en que:

  1. están encerradas en etiquetas {define}
  2. sólo se muestran cuando se insertan a través de {include}
  3. se pueden definir parámetros para ellas como las funciones en 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> *}

Imagina que tienes una plantilla de ayuda con una colección de definiciones sobre cómo dibujar formularios 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}

Los argumentos de una definición son siempre opcionales con valor por defecto null, a menos que se especifique un valor por defecto (aquí 'text' es el valor por defecto de $type). También se pueden declarar tipos de parámetros: {define input, string $name, ...}.

La plantilla con las definiciones se carga utilizando {import}. Las propias definiciones se renderizan del mismo modo que los bloques:

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

Las definiciones no tienen acceso a las variables del contexto activo, pero sí a las variables globales.

Nombres de bloques dinámicos

Latte permite una gran flexibilidad en la definición de bloques porque el nombre del bloque puede ser cualquier expresión PHP. Este ejemplo define tres bloques llamados hi-Peter, hi-John y hi-Mary:

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

Por ejemplo, podemos redefinir sólo un bloque en una plantilla hija:

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

Así que la salida tendrá este aspecto:

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

Comprobación de la existencia de bloques {ifset}

Véase también {ifset $var}

Utilice la prueba {ifset blockname} para comprobar si existe un bloque (o más bloques) en el contexto actual:

{ifset footer}
	...
{/ifset}

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

Puede utilizar una variable o cualquier expresión en PHP como nombre del bloque. En este caso, añada la palabra clave block antes de la variable para dejar claro que no es la variable lo que se comprueba:

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

La existencia de bloques también se devuelve mediante la función hasBlock():

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

Consejos

Aquí tienes algunos consejos para trabajar con bloques:

  • El último bloque de nivel superior no necesita tener etiqueta de cierre (el bloque termina con el final del documento). Esto simplifica la escritura de plantillas hijo, que un bloque primario.
  • Para una mayor legibilidad, puede dar opcionalmente un nombre a su etiqueta {/block}, por ejemplo {/block footer}. Sin embargo, el nombre debe coincidir con el nombre del bloque. En plantillas más grandes, esta técnica ayuda a ver qué etiquetas de bloque se están cerrando.
  • No es posible definir directamente varias etiquetas de bloque con el mismo nombre en la misma plantilla. Pero se puede conseguir utilizando nombres de bloque dinámicos.
  • Puede utilizar n:attributes para definir bloques como <h1 n:block=title>Welcome to my awesome homepage</h1>
  • Los bloques también pueden utilizarse sin nombre sólo para aplicar los filtros a la salida: {block|strip} hello {/block}

Reutilización horizontal {import}

La reutilización horizontal es un tercer mecanismo de reutilización y herencia en Latte. Permite cargar bloques de otras plantillas. Es similar a crear un archivo PHP con funciones de ayuda o un trait.

Aunque la herencia del diseño de las plantillas es una de las características más potentes de Latte, está limitada a la herencia simple: una plantilla sólo puede extender a otra plantilla. La reutilización horizontal es una forma de lograr una herencia múltiple.

Tomemos un conjunto de definiciones de bloque:

{block sidebar}...{/block}

{block menu}...{/block}

Utilizando el comando {import}, importe todos los bloques y definiciones definidos en blocks.latte a otra plantilla:

{import 'blocks.latte'}

{* ahora se pueden utilizar los bloques de barra lateral y menú *}

Si importa los bloques en la plantilla padre (es decir, si utiliza {import} en layout.latte), los bloques también estarán disponibles en todas las plantillas hijas, lo que resulta muy práctico.

La plantilla que se pretende importar (por ejemplo, blocks.latte) no debe ampliar otra plantilla, es decir, utilizar {layout}. Sin embargo, puede importar otras plantillas.

La etiqueta {import} debe ser la primera etiqueta de plantilla después de {layout}. El nombre de la plantilla puede ser cualquier expresión PHP:

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

Puede utilizar tantas expresiones {import} como desee en una plantilla determinada. Si dos plantillas importadas definen el mismo bloque, gana la primera. Sin embargo, la mayor prioridad la tiene la plantilla principal, que puede sobrescribir cualquier bloque importado.

Todos los bloques sobrescritos pueden incluirse gradualmente insertándolos como bloque padre:

{layout 'layout.latte'}

{import 'blocks.latte'}

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

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

En este ejemplo, {include parent} llamará correctamente al bloque sidebar desde la plantilla blocks.latte.

Herencia de unidades {embed}

La herencia de unidades lleva la idea de la herencia de diseño al nivel de los fragmentos de contenido. Mientras que la herencia de maquetación funciona con “esqueletos de documento”, a los que dan vida plantillas hijas, la herencia de unidades te permite crear esqueletos para unidades de contenido más pequeñas y reutilizarlos donde quieras.

En la herencia de unidades, la etiqueta {embed} es la clave. Combina el comportamiento de {include} y {layout}. Permite incluir el contenido de otra plantilla o bloque y, opcionalmente, pasar variables, al igual que {include}. También le permite anular cualquier bloque definido dentro de la plantilla incluida, como hace {layout}.

Por ejemplo, vamos a utilizar el elemento acordeón plegable. Echemos un vistazo al esqueleto del elemento en la plantilla collapsible.latte:

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

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

Las etiquetas {block} definen dos bloques que las plantillas hijas pueden rellenar. Sí, como en el caso de la plantilla padre en la plantilla de herencia de diseño. También puede ver $modifierClass variable.

Vamos a utilizar nuestro elemento en la plantilla. Aquí es donde entra {embed}. Es una pieza super potente que nos permite hacer todas las cosas: incluir el contenido de la plantilla del elemento, añadirle variables y añadirle bloques con HTML personalizado:

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

La salida podría parecerse 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>

Los bloques dentro de etiquetas embed forman una capa separada independiente de otros bloques. Por lo tanto, pueden tener el mismo nombre que el bloque fuera del embed y no se ven afectados de ninguna manera. Utilizando la etiqueta include dentro de las etiquetas {embed} puede insertar bloques creados aquí, bloques de la plantilla incrustada (que no son locales), y también bloques de la plantilla principal que son locales. También puede importar bloques de otros archivos:

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

Las plantillas incrustadas no tienen acceso a las variables del contexto activo, pero sí a las variables globales.

Con {embed} puede insertar no sólo plantillas sino también otros bloques, por lo que el ejemplo anterior podría escribirse así:

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

Si pasamos una expresión a {embed} y no está claro si es un bloque o un nombre de archivo, añada la palabra clave block o file:

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

Casos de uso

Existen varios tipos de herencia y reutilización de código en Latte. Vamos a resumir los conceptos principales para mayor claridad:

{include template}

Caso de uso: Usando header.latte & footer.latte dentro de 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 práctico: Extensión de layout.latte dentro de homepage.latte y 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 de uso: sidebar.latte en single.product.latte & 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 de uso: Una función que obtiene algunas variables y produce algunas marcas.

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 práctico: Incrustación de pagination.latte en product.table.latte y 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}
versión: 3.0