Herencia y reutilización de plantillas
Los mecanismos de reutilización y herencia de plantillas están aquí para aumentar su productividad, porque cada plantilla contiene solo su contenido único y los elementos y estructuras repetidos se reutilizan. Presentamos tres conceptos: Layout Inheritance, Horizontal Reuse y Unit Inheritance.
El concepto de herencia de plantillas de Latte se parece a la herencia de clases de PHP. Usted define una plantilla padre de la que pueden heredar otras plantillas hijas, que pueden sobrescribir partes de la plantilla padre. Funciona de maravilla cuando los elementos comparten una estructura común. ¿Suena complicado? No se preocupe, es muy fácil.
Herencia de layout {layout}
Veamos la herencia de la plantilla de layout con un ejemplo. Esta es una plantilla padre, llamémosla
layout.latte, que define el esqueleto de 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>
Las etiquetas {block} definen tres bloques que las plantillas hijas pueden rellenar. Lo único que hace la
etiqueta block es decirle al motor de plantillas que una plantilla hija puede sobrescribir esas partes 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 aquí la clave. Le dice a Latte que esta plantilla “extiende” otra plantilla. Cuando
Latte renderiza esta plantilla, primero localiza la plantilla padre, en este caso layout.latte.
Llegado ese punto, Latte se fija en las tres etiquetas block de layout.latte y sustituye esos bloques por el
contenido de la plantilla hija. Como la plantilla hija no definía el bloque footer, se usa en su lugar el contenido de la
plantilla padre. El contenido dentro de una etiqueta {block} de la plantilla padre se usa siempre como valor de
reserva.
La salida podría tener este aspecto:
<!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">
© Copyright 2008
</div>
</body>
</html>
En una plantilla hija, los bloques suelen colocarse en el nivel superior o dentro de otro bloque, por ejemplo:
{block content}
<h1>{block title}Welcome to my awesome homepage{/block}</h1>
{/block}
Además, un bloque se crea siempre, con independencia de que la condición {if} circundante se evalúe como
verdadera o falsa. Así que, aunque no lo parezca, esta plantilla sí define el bloque.
{if false}
{block head}
<meta name="robots" content="noindex, follow">
{/block}
{/if}
Si quiere que la salida del interior del bloque se muestre de forma condicional, use esto en su lugar:
{block head}
{if $condition}
<meta name="robots" content="noindex, follow">
{/if}
{/block}
El código de la cabecera de la plantilla hija (es decir, antes del primer bloque o de cualquier salida) se ejecuta antes de
renderizar la plantilla de layout, así que puede usarlo para definir variables como {var $foo = bar} y propagar
datos por toda la cadena de herencia. El código colocado entre bloques o después de ellos en una plantilla con
{layout} no se ejecuta en absoluto:
{layout 'layout.latte'}
{var $robots = noindex}
...
Si quiere pasar variables solo al layout, sin crearlas en la plantilla actual, indíquelas directamente en
la etiqueta {layout} (o {extends}) tras una coma:
{layout 'layout.latte', robots: noindex}
La variable $robots estará disponible en el layout y en sus bloques, pero no en los bloques de la plantilla
actual. Una variable pasada explícitamente tiene además prioridad sobre un parámetro de plantilla del mismo nombre.
Herencia multinivel
Puede usar tantos niveles de herencia como necesite. Una forma habitual de usar la herencia de layout es este enfoque en tres niveles:
- Cree una plantilla
layout.latteque contenga el aspecto general de su sitio. - Cree una plantilla
layout-SECTIONNAME.lattepara cada sección de su sitio. Por ejemplo,layout-news.latte,layout-blog.latte, etc. Todas estas plantillas extiendenlayout.lattee incluyen los estilos y el diseño propios de cada sección. - Cree plantillas individuales para cada tipo de página, como un artículo de noticias o una entrada de blog. Estas plantillas extienden la plantilla de sección correspondiente.
Herencia de layout dinámica
Puede usar una variable o cualquier expresión de PHP como nombre de la plantilla padre, de modo que la herencia se comporte de forma dinámica:
{layout $standalone ? 'minimum.latte' : 'layout.latte'}
También puede usar la API de Latte para elegir la plantilla de layout automáticamente.
Consejos
Aquí tiene algunos consejos para trabajar con la herencia de layout:
- Si usa
{layout}en una plantilla, debe estar en la cabecera de la plantilla, es decir, antes de cualquier salida. Solo pueden precederlo etiquetas que no producen salida (como{var},{templateType},{import}o los comentarios). - El layout se puede buscar automáticamente
(como en los presenters). En tal caso, si
la plantilla no debe tener layout, lo indica con la etiqueta
{layout none}. Al contrario,{layout auto}(o{extends auto}) restablece la búsqueda automática del layout. - La etiqueta
{layout}tiene el alias{extends}. - El nombre del archivo de layout depende del loader.
- Puede tener tantos bloques como quiera. Recuerde que las plantillas hijas no tienen por qué definir todos los bloques del padre, así que puede rellenar valores razonables en varios bloques y definir después solo los que necesite.
Bloques {block}
Vea también el {block} anónimo
Un bloque ofrece una forma de cambiar cómo se renderiza cierta parte de una plantilla, pero no interfiere en absoluto con la lógica que la rodea. Ilustremos cómo funciona un bloque y, sobre todo, cómo no funciona, con el siguiente ejemplo:
{foreach $posts as $post}
{block post}
<h1>{$post->title}</h1>
<p>{$post->body}</p>
{/block}
{/foreach}
Si renderiza esta plantilla, el resultado será exactamente el mismo con o sin las etiquetas {block}. Los bloques
tienen acceso a las variables de los ámbitos exteriores. Solo ofrecen una forma de ser sobrescritos 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 usará el bloque definido en la plantilla hija child.latte en
lugar del definido en parent.latte; la plantilla ejecutada equivale entonces a esto:
{foreach $posts as $post}
<article>
<header>{$post->title}</header>
<section>{$post->text}</section>
</article>
{/foreach}
Ahora bien, si creamos una variable nueva dentro de un bloque con nombre o sustituimos el valor de una existente, el cambio solo será visible dentro del bloque:
{var $foo = 'foo'}
{block post}
{do $foo = 'new value'}
{var $bar = 'bar'}
{/block}
foo: {$foo} // muestra: foo
bar: {$bar ?? 'not defined'} // muestra: not defined
El contenido de un bloque se puede modificar con filtros. El siguiente ejemplo elimina todo el HTML y lo pasa a mayúsculas:
<title>{block title|stripHtml|capitalize}...{/block}</title>
La etiqueta también se puede escribir como n:atributo:
<article n:block=post>
...
</article>
Bloques locales
Todo bloque sobrescribe el contenido del bloque padre con el mismo nombre, salvo los bloques locales. Son análogos a los métodos privados de las clases. Puede crear una plantilla sin temer que, por una coincidencia de nombres de bloque, otra plantilla los sobrescriba.
{block local helper}
...
{/block}
Imprimir bloques {include}
Vea también {include file}
Para imprimir un bloque en un lugar concreto, use la etiqueta {include blockname}:
<title>{block title}{/block}</title>
<h1>{include title}</h1>
También puede imprimir un bloque de otra plantilla:
{include footer from 'main.latte'}
El bloque renderizado no tiene acceso a las variables del contexto activo, salvo que el bloque esté definido en el mismo archivo donde se incluye. Sí tiene acceso, en cambio, a las variables globales.
Puede pasar variables al bloque así:
{include footer, foo: bar, id: 123}
El nombre del bloque puede ser una variable o cualquier expresión de PHP. En ese caso, añada la palabra clave
block delante de la variable para que Latte sepa en tiempo de compilación que se trata de un bloque y no de una plantilla incluida, cuyo nombre también podría estar en una variable:
{var $name = footer}
{include block $name}
Un bloque también se puede renderizar dentro de sí mismo, lo que resulta útil, por ejemplo, al renderizar 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 el bloque actual.
El contenido renderizado de un bloque se puede modificar con filtros. El siguiente ejemplo elimina todo el HTML y lo pasa a mayúsculas:
<title>{include heading|stripHtml|capitalize}</title>
Bloque padre
Si necesita imprimir el contenido del bloque de la plantilla padre, use {include parent}. Resulta útil cuando
quiere completar el contenido del bloque padre en lugar de sobrescribirlo 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, Latte tiene también “definiciones”. En los lenguajes de programación habituales serían comparables a las funciones. Sirven para reutilizar fragmentos de plantilla y evitar repeticiones.
Latte intenta mantener las cosas sencillas, así que en el fondo las definiciones son lo mismo que los bloques, y todo lo dicho sobre los bloques vale también para las definiciones. Se diferencian de los bloques en que:
- se encierran en etiquetas
{define} - se renderizan solo cuando se insertan con
{include} - se les pueden definir parámetros, como a las funciones de PHP
{block foo}<p>Hello</p>{/block}
{* muestra: <p>Hello</p> *}
{define bar}<p>World</p>{/define}
{* no muestra nada *}
{include bar}
{* muestra: <p>World</p> *}
Imagine que tiene una plantilla auxiliar 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 son siempre opcionales, con el valor predeterminado null, salvo que se indique un valor
predeterminado (aquí 'text' es el valor predeterminado de $type). También se pueden declarar los tipos
de los parámetros: {define input, string $name, ...}.
La plantilla con las definiciones se carga con {import}. Las propias
definiciones se renderizan igual que los bloques:
<p>{include input, 'password', null, 'password'}</p>
<p>{include textarea, 'comment'}</p>
Igual que los bloques, las definiciones no tienen acceso a las variables del contexto activo, solo a las variables globales. La excepción es una definición que no tiene parámetros declarados, se referencia por un nombre estático y se incluye en el mismo archivo donde está definida: esa definición sí tiene acceso a las variables de contexto del lugar desde el que se incluye.
Nombres de bloque dinámicos
Latte permite una gran flexibilidad al definir bloques, porque el nombre del bloque puede ser cualquier expresión de 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}
En la plantilla hija podemos redefinir después, por ejemplo, solo un bloque:
{block hi-John}Hello. I am {$name}.{/block}
Así, la salida quedará así:
Hi, I am Peter.
Hello. I am John.
Hi, I am Mary.
Comprobar la existencia de un bloque {ifset}
Vea también {ifset $var}
Use la prueba {ifset blockname} para comprobar si un bloque (o varios bloques) existe en el contexto actual:
{ifset footer}
...
{/ifset}
{ifset footer, header, main}
...
{/ifset}
El nombre del bloque puede ser una variable o cualquier expresión de PHP. En ese caso, añada la palabra clave
block delante de la variable para dejar claro que no se trata de comprobar la existencia de variables:
{ifset block $name}
...
{/ifset}
La existencia de bloques la comprueba también la función hasBlock():
{if hasBlock(header) || hasBlock(footer)}
...
{/if}
Consejos
Algunos consejos para trabajar con bloques:
- El último bloque de nivel superior no necesita etiqueta de cierre (el bloque termina con el final del documento). Esto simplifica escribir plantillas hijas que contienen un bloque principal.
- Para mejorar la legibilidad puede indicar opcionalmente el nombre del bloque en la etiqueta
{/block}, por ejemplo{/block footer}. Eso sí, el nombre debe coincidir con el del bloque. En plantillas grandes, esta técnica ayuda a ver qué etiquetas de bloque se están cerrando. - No puede definir directamente varias etiquetas de bloque con el mismo nombre en la misma plantilla. Pero se puede lograr con los Nombres de bloque dinámicos.
- Puede usar n:atributos para definir bloques,
como
<h1 n:block=title>Welcome to my awesome homepage</h1> - Los bloques también se pueden usar sin nombre, solo para aplicar filtros a la salida:
{block|strip} hello {/block}
Reutilización horizontal {import}
La reutilización horizontal es el tercer mecanismo de reutilización y herencia de Latte. Permite cargar bloques de otras
plantillas. Se parece a crear en PHP un archivo con funciones auxiliares y cargarlo después con require.
Aunque la herencia de layout es una de las funciones más potentes de Latte, se limita a la herencia simple: una plantilla solo puede extender otra plantilla. La reutilización horizontal es una forma de conseguir herencia múltiple.
Tengamos un archivo con definiciones de bloques:
{block sidebar}...{/block}
{block menu}...{/block}
Con el comando {import} importamos a otra plantilla todos los bloques y Definitions
definidos en blocks.latte:
{import 'blocks.latte'}
{* ahora se pueden usar los bloques sidebar y menu *}
Si importa los bloques en la plantilla padre (es decir, usa {import} en layout.latte), los bloques
estarán disponibles también en todas las plantillas hijas, lo que resulta muy práctico.
La plantilla destinada a ser importada (por ejemplo, blocks.latte) no debe extender otra plantilla, es decir, no debe usar {layout}. Sí puede, en cambio,
importar otras plantillas.
La etiqueta {import} debería ser la primera etiqueta de la plantilla después de {layout}. El nombre
de la plantilla puede ser cualquier expresión de PHP:
{import $ajax ? 'ajax.latte' : 'not-ajax.latte'}
Puede usar tantas sentencias {import} como quiera en una plantilla. Si dos plantillas importadas definen el mismo
bloque, gana la primera. Ahora bien, la plantilla principal tiene la máxima prioridad y puede sobrescribir cualquier bloque
importado.
La etiqueta {import} también puede pasar argumentos a la plantilla importada, por ejemplo
{import 'blocks.latte', foo: 1}. Esos argumentos están después disponibles como variables en los bloques y
definiciones importados.
El contenido de los bloques sobrescritos se puede conservar insertando el bloque igual que un Bloque padre:
{layout 'layout.latte'}
{import 'blocks.latte'}
{block sidebar}
{include parent}
{/block}
{block title}...{/block}
{block content}...{/block}
En este ejemplo, {include parent} llama al bloque sidebar de la plantilla
blocks.latte.
Herencia de unidades {embed}
La herencia de unidades lleva la idea de la herencia de layout al nivel de los fragmentos de contenido. Mientras que la herencia de layout trabaja con “esqueletos de documento” que las plantillas hijas dan vida, la herencia de unidades permite crear esqueletos para unidades de contenido más pequeñas y reutilizarlos donde quiera.
En la herencia de unidades, la etiqueta clave es {embed}. Combina el comportamiento de {include} y de
{layout}. Permite incrustar el contenido de otra plantilla o bloque y, opcionalmente, pasar variables, igual que
{include}. Y permite además sobrescribir cualquier bloque definido dentro de la plantilla incrustada, como
{layout}.
Usemos, por ejemplo, un elemento acordeón. Eche un vistazo al esqueleto del elemento, guardado 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í, igual que en el caso de
la plantilla padre en la herencia de layout. También ve la variable $modifierClass.
Usemos nuestro elemento en una plantilla. Aquí entra en juego {embed}. Es una etiqueta extremadamente potente que
nos permite hacer todo esto: incrustar el contenido de la plantilla del elemento, añadirle variables y añadirle bloques con HTML
propio:
{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 tener este aspecto:
<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 las etiquetas embed forman una capa aparte, aislada de los bloques exteriores al embed. Por eso pueden
tener el mismo nombre que un bloque de fuera sin que colisionen, y este no les afecta. Con la etiqueta include dentro de las etiquetas {embed} puede insertar los bloques creados aquí, los
bloques de la plantilla incrustada (que no sean locales) y también los bloques de la
plantilla principal que sí sean 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} {* funciona, el bloque está definido dentro de embed *}
{include hello} {* funciona, el bloque es local en esta plantilla *}
{include content} {* funciona, el bloque está definido en la plantilla incrustada *}
{include aBlockDefinedInImportedTemplate} {* funciona *}
{include outer} {* ¡no funciona! - el bloque está en la capa exterior *}
{/block}
{/embed}
Las plantillas incrustadas no tienen acceso a las variables del contexto activo, pero sí a las variables globales.
Con {embed} puede incrustar no solo plantillas, sino también otros bloques, de modo 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}
Hay, no obstante, una diferencia entre ambos: cuando incrusta un bloque en lugar de un archivo, los bloques de la capa exterior
siguen siendo accesibles dentro del embed. Así que, a diferencia de lo que ocurre con un archivo incrustado, allí
{include outer} sí funcionaría.
Si pasamos una expresión a {embed} y no queda claro si es un nombre de bloque o de archivo, añada la palabra
clave block o file:
{embed block $name} ... {/embed}
Casos de uso
En Latte hay varios tipos de herencia y de reutilización de código. Resumamos los conceptos principales para mayor claridad:
{include template}
Caso de uso: usar header.latte y 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 de uso: extender 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: usar sidebar.latte en single.product.latte y
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: funciones que reciben variables y renderizan algo.
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 de uso: incrustar 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}