Vererbung und Wiederverwendbarkeit von Vorlagen
Die Wiederverwendbarkeit von Vorlagen und die Vererbungsmechanismen sind dazu da, Ihre Produktivität zu steigern, da jede Vorlage nur ihren einzigartigen Inhalt enthält und die sich wiederholenden Elemente und Strukturen wiederverwendet werden. Wir stellen drei Konzepte vor: Layout-Vererbung, horizontale Wiederverwendung und Unit-Vererbung.
Das Konzept der Latte-Vorlagenvererbung ähnelt der PHP-Klassenvererbung. Sie definieren eine Elternvorlage, von der andere Kindvorlagen ableiten und Teile der Elternvorlage außer Kraft setzen können. Es funktioniert hervorragend, wenn Elemente eine gemeinsame Struktur haben. Klingt kompliziert? Keine Sorge, das ist es nicht.
Layout-Vererbung {layout}
Lassen Sie uns die Vererbung von Layoutvorlagen anhand eines Beispiels betrachten. Dies ist eine übergeordnete Vorlage, die
wir zum Beispiel layout.latte
nennen und die ein HTML-Skelettdokument definiert.
<!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>
Die {block}
Tags definieren drei Blöcke, die von untergeordneten Vorlagen ausgefüllt werden können. Mit dem
Block-Tag wird der Vorlagen-Engine lediglich mitgeteilt, dass eine untergeordnete Vorlage diese Teile der Vorlage außer Kraft
setzen kann, indem sie einen eigenen Block mit demselben Namen definiert.
Eine untergeordnete Vorlage könnte wie folgt aussehen:
{layout 'layout.latte'}
{block title}My amazing blog{/block}
{block content}
<p>Welcome to my awesome homepage.</p>
{/block}
Der {layout}
Tag ist hier der Schlüssel. Es teilt der Template-Engine mit, dass diese Vorlage eine andere Vorlage
“erweitert”. Wenn Latte diese Vorlage rendert, sucht es zunächst die übergeordnete Vorlage – in diesem Fall
layout.latte
.
An diesem Punkt bemerkt die Template-Engine die drei Block-Tags in layout.latte
und ersetzt diese Blöcke durch
den Inhalt der Kind-Vorlage. Da die untergeordnete Vorlage den Fußzeilenblock nicht definiert hat, wird stattdessen der
Inhalt der übergeordneten Vorlage verwendet. Der Inhalt innerhalb eines {block}
-Tags in einer übergeordneten
Vorlage wird immer als Fallback verwendet.
Die Ausgabe könnte wie folgt aussehen:
<!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>
In einer untergeordneten Vorlage können sich Blöcke nur entweder auf der obersten Ebene oder innerhalb eines anderen Blocks befinden, d. h:
{block content}
<h1>{block title}Welcome to my awesome homepage{/block}</h1>
{/block}
Außerdem wird immer ein Block erstellt, unabhängig davon, ob die umgebende Bedingung {if}
als wahr oder falsch
ausgewertet wird. Im Gegensatz zu dem, was Sie vielleicht denken, definiert diese Vorlage tatsächlich einen Block.
{if false}
{block head}
<meta name="robots" content="noindex, follow">
{/block}
{/if}
Wenn Sie möchten, dass die Ausgabe innerhalb des Blocks nur unter bestimmten Bedingungen angezeigt wird, verwenden Sie stattdessen das Folgende:
{block head}
{if $condition}
<meta name="robots" content="noindex, follow">
{/if}
{/block}
Daten außerhalb eines Blocks in einer untergeordneten Vorlage werden ausgeführt, bevor die Layoutvorlage gerendert wird. Sie
können damit Variablen wie {var $foo = bar}
definieren und Daten an die gesamte Vererbungskette weitergeben:
{layout 'layout.latte'}
{var $robots = noindex}
...
Mehrstufige Vererbung
Sie können so viele Vererbungsebenen verwenden, wie Sie benötigen. Eine gängige Methode zur Verwendung der Layoutvererbung ist der folgende dreistufige Ansatz:
- Erstellen Sie eine Vorlage
layout.latte
, die das Hauptdesign Ihrer Website enthält. - Erstellen Sie eine Vorlage
layout-SECTIONNAME.latte
für jeden Abschnitt Ihrer Website. Zum Beispiel:layout-news.latte
,layout-blog.latte
usw. Diese Vorlagen erweitern allelayout.latte
und enthalten abschnittsspezifische Stile/Designs. - Erstellen Sie individuelle Vorlagen für jede Art von Seite, z. B. für einen Nachrichtenartikel oder einen Blogeintrag. Diese Vorlagen erweitern die entsprechende Bereichsvorlage.
Dynamische Layout-Vererbung
Sie können eine Variable oder einen beliebigen PHP-Ausdruck als Namen der übergeordneten Vorlage verwenden, so dass sich die Vererbung dynamisch verhalten kann:
{layout $standalone ? 'minimum.latte' : 'layout.latte'}
Sie können auch die Latte-API verwenden, um die Layout-Vorlage automatisch auszuwählen.
Tipps
Hier finden Sie einige Tipps für die Arbeit mit Layoutvererbung:
- Wenn Sie
{layout}
in einer Vorlage verwenden, muss es das erste Vorlagen-Tag in dieser Vorlage sein. - Das Layout kann automatisch gesucht werden (wie
bei Präsentationen). In diesem Fall, wenn die
Vorlage kein Layout haben sollte, wird dies mit dem Tag
{layout none}
angezeigt. - Das Tag
{layout}
hat den Alias{extends}
. - Der Dateiname der erweiterten Vorlage hängt vom Vorlagenlader ab.
- Sie können so viele Blöcke haben, wie Sie wollen. Denken Sie daran, dass untergeordnete Vorlagen nicht alle übergeordneten Blöcke definieren müssen. Sie können also in einer Reihe von Blöcken vernünftige Vorgaben machen und dann nur die Blöcke definieren, die Sie später benötigen.
Blöcke {block}
Siehe auch Anonymität {block}
Ein Block bietet eine Möglichkeit, die Darstellung eines bestimmten Teils einer Vorlage zu ändern, greift aber in keiner Weise in die Logik der Vorlage ein. Anhand des folgenden Beispiels soll gezeigt werden, wie ein Block funktioniert und, was noch wichtiger ist, wie er nicht funktioniert:
{foreach $posts as $post}
{block post}
<h1>{$post->title}</h1>
<p>{$post->body}</p>
{/block}
{/foreach}
Wenn Sie diese Vorlage rendern, wäre das Ergebnis mit oder ohne die Block-Tags genau dasselbe. Blöcke haben Zugriff auf Variablen aus äußeren Bereichen. Dies ist nur eine Möglichkeit, sie durch eine untergeordnete Vorlage überschreibbar zu machen:
{layout 'parent.Latte'}
{block post}
<article>
<header>{$post->title}</header>
<section>{$post->text}</section>
</article>
{/block}
Beim Rendern der Child-Vorlage verwendet die Schleife nun den in der Child-Vorlage child.Latte
definierten Block
anstelle des in der Basis-Vorlage parent.Latte
definierten Blocks; die ausgeführte Vorlage entspricht dann der
folgenden:
{foreach $posts as $post}
<article>
<header>{$post->title}</header>
<section>{$post->text}</section>
</article>
{/foreach}
Wenn wir jedoch eine neue Variable innerhalb eines benannten Blocks erstellen oder einen Wert einer bestehenden Variable ersetzen, wird die Änderung nur innerhalb des Blocks sichtbar:
{var $foo = 'foo'}
{block post}
{do $foo = 'new value'}
{var $bar = 'bar'}
{/block}
foo: {$foo} // prints: foo
bar: {$bar ?? 'not defined'} // prints: not defined
Der Inhalt des Blocks kann durch Filter geändert werden. Das folgende Beispiel entfernt den gesamten HTML-Code und setzt ihn in Großbuchstaben:
<title>{block title|stripHtml|capitalize}...{/block}</title>
Der Tag kann auch als n:attribute geschrieben werden:
<article n:block=post>
...
</article>
Lokale Blöcke
Jeder Block überschreibt den Inhalt des übergeordneten Blocks mit demselben Namen. Außer bei lokalen Blöcken. Sie sind so etwas wie private Methoden in einer Klasse. Sie können eine Vorlage erstellen, ohne befürchten zu müssen, dass sie – aufgrund der Übereinstimmung der Blocknamen – von der zweiten Vorlage überschrieben werden.
{block local helper}
...
{/block}
Blöcke drucken {include}
Siehe auch {include file}
Um einen Block an einer bestimmten Stelle zu drucken, verwenden Sie das Tag {include blockname}
:
<title>{block title}{/block}</title>
<h1>{include title}</h1>
Sie können auch einen Block aus einer anderen Vorlage anzeigen:
{include footer from 'main.latte'}
Gedruckte Blöcke haben keinen Zugriff auf die Variablen des aktiven Kontexts, es sei denn, der Block ist in derselben Datei definiert, in die er eingebunden ist. Sie haben jedoch Zugriff auf die globalen Variablen.
Sie können Variablen auf folgende Weise an den Block übergeben:
{include footer, foo: bar, id: 123}
Sie können eine Variable oder einen beliebigen Ausdruck in PHP als Blocknamen verwenden. Fügen Sie in diesem Fall das
Schlüsselwort block
vor der Variablen ein, damit zur Kompilierzeit bekannt ist, dass es sich um einen Block handelt
und nicht um eine Vorlage, deren Name auch in der Variablen stehen
könnte:
{var $name = footer}
{include block $name}
Ein Block kann auch in sich selbst ausgedruckt werden, was z. B. bei der Darstellung einer Baumstruktur nützlich ist:
{define menu, $items}
<ul>
{foreach $items as $item}
<li>
{if is_array($item)}
{include menu, $item}
{else}
{$item}
{/if}
</li>
{/foreach}
</ul>
{/define}
Anstelle von {include menu, ...}
können wir auch {include this, ...}
schreiben, wobei
this
für den aktuellen Block steht.
Der gedruckte Inhalt kann durch Filter verändert werden. Das folgende Beispiel entfernt den gesamten HTML-Code und setzt ihn in Großbuchstaben:
<title>{include heading|stripHtml|capitalize}</title>
Übergeordneter Block
Wenn Sie den Inhalt des Blocks aus der übergeordneten Vorlage ausgeben möchten, können Sie dies mit der Anweisung
{include parent}
tun. Dies ist nützlich, wenn Sie den Inhalt eines übergeordneten Blocks ergänzen möchten,
anstatt ihn vollständig zu überschreiben.
{block footer}
{include parent}
<a href="https://github.com/nette">GitHub</a>
<a href="https://twitter.com/nettefw">Twitter</a>
{/block}
Definitionen {define}
Zusätzlich zu den Blöcken gibt es in Latte auch “Definitionen”. Sie sind vergleichbar mit Funktionen in regulären Programmiersprachen. Sie sind nützlich, um Vorlagenfragmente wiederzuverwenden und sich nicht zu wiederholen.
Latte versucht, die Dinge einfach zu halten, daher sind Definitionen im Grunde dasselbe wie Blöcke, und alles, was über Blöcke gesagt wird, gilt auch für Definitionen. Darin unterscheiden sie sich von Blöcken:
- sie in Tags eingeschlossen sind
{define}
- sie nur gerendert werden, wenn sie eingefügt werden über
{include}
- Sie können Parameter für sie definieren, wie Funktionen 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> *}
Stellen Sie sich vor, Sie haben eine Hilfsvorlage mit einer Sammlung von Definitionen, wie man HTML-Formulare zeichnet.
{define input, $name, $value, $type = 'text'}
<input type={$type} name={$name} value={$value}>
{/define}
{define textarea, $name, $value}
<textarea name={$name}>{$value}</textarea>
{/define}
Argumente einer Definition sind immer optional mit dem Standardwert null
, es sei denn, es wird ein Standardwert
angegeben (hier ist 'text'
der Standardwert für $type
). Es können auch Parametertypen deklariert
werden: {define input, string $name, ...}
.
Die Vorlage mit den Definitionen wird geladen mit {import}
. Die Definitionen
selbst werden auf die gleiche Weise gerendert wie die
Blöcke:
<p>{include input, 'password', null, 'password'}</p>
<p>{include textarea, 'comment'}</p>
Definitionen haben keinen Zugriff auf die Variablen des aktiven Kontexts, aber sie haben Zugriff auf globale Variablen.
Dynamische Block-Namen
Latte erlaubt eine große Flexibilität bei der Definition von Blöcken, da der Blockname ein beliebiger PHP-Ausdruck sein
kann. In diesem Beispiel werden drei Blöcke mit den Namen hi-Peter
, hi-John
und hi-Mary
definiert:
{foreach [Peter, John, Mary] as $name}
{block "hi-$name"}Hallo, ich bin {$name}.{/block}
{/foreach}
Wir können zum Beispiel nur einen Block in einer Child-Vorlage neu definieren:
{block hi-John}Hallo. Ich bin {$name}.{/block}
Die Ausgabe sieht dann wie folgt aus:
Hi, I am Peter.
Hello. I am John.
Hi, I am Mary.
Überprüfung der Blockexistenz {ifset}
Siehe auch {ifset $var}
Verwenden Sie den {ifset blockname}
Test, um zu prüfen, ob ein Block (oder mehrere Blöcke) im aktuellen Kontext
existiert:
{ifset footer}
...
{/ifset}
{ifset footer, header, main}
...
{/ifset}
Sie können eine Variable oder einen beliebigen Ausdruck in PHP als Blocknamen verwenden. Fügen Sie in diesem Fall das
Schlüsselwort block
vor der Variablen ein, um klarzustellen, dass es sich nicht um die Variable handelt, die geprüft wird:
{ifset block $name}
...
{/ifset}
Die Existenz von Blöcken wird auch von der Funktion hasBlock()
:
{if hasBlock(header) || hasBlock(footer)}
...
{/if}
Tipps
Hier finden Sie einige Tipps für die Arbeit mit Blöcken:
- Der letzte Block der obersten Ebene braucht keinen schließenden Tag zu haben (der Block endet mit dem Ende des Dokuments). Dies vereinfacht das Schreiben von untergeordneten Vorlagen, die einen Primärblock enthalten.
- Um die Lesbarkeit zu erhöhen, können Sie Ihrem
{/block}
-Tag optional einen Namen geben, z. B.{/block footer}
. Der Name muss jedoch mit dem Blocknamen übereinstimmen. In größeren Vorlagen hilft Ihnen diese Technik zu sehen, welche Block-Tags geschlossen werden. - Sie können nicht direkt mehrere Block-Tags mit demselben Namen in derselben Vorlage definieren. Dies kann jedoch mit dynamischen Blocknamen erreicht werden.
- Sie können n:attributes verwenden, um Blöcke zu definieren
wie
<h1 n:block=title>Welcome to my awesome homepage</h1>
- Blöcke können auch ohne Namen verwendet werden, nur um die Filter auf die Ausgabe
anzuwenden:
{block|strip} hello {/block}
Horizontale Wiederverwendung {import}
Die horizontale Wiederverwendung ist der dritte Mechanismus für Wiederverwendung und Vererbung in Latte. Er ermöglicht das
Laden von Blöcken aus anderen Vorlagen. Dies ist vergleichbar mit der Erstellung einer Datei mit Hilfsfunktionen in PHP und dem
anschließenden Laden mit require
.
Obwohl die Vererbung von Vorlagenlayouts eine der leistungsfähigsten Funktionen von Latte ist, ist sie auf einfache Vererbung beschränkt – eine Vorlage kann nur eine andere Vorlage erweitern. Die horizontale Wiederverwendung ist eine Möglichkeit, Mehrfachvererbung zu erreichen.
Nehmen wir einen Satz von Blockdefinitionen:
{block sidebar}...{/block}
{block menu}...{/block}
Mit dem Befehl {import}
importieren Sie alle in blocks.latte
definierten Blöcke und Definitionen in eine andere Vorlage:
{import 'blocks.latte'}
{* Seitenleisten- und Menüblöcke können jetzt verwendet werden *}
Wenn Sie die Blöcke in die übergeordnete Vorlage importieren (d. h. {import}
in layout.latte
verwenden), sind die Blöcke auch in allen untergeordneten Vorlagen verfügbar, was sehr praktisch ist.
Die Vorlage, die importiert werden soll (z. B. blocks.latte
), darf keine andere Vorlage erweitern, d. h. {layout}
verwenden. Sie kann jedoch andere Vorlagen
importieren.
Das Tag {import}
sollte das erste Template-Tag nach {layout}
sein. Der Vorlagenname kann ein
beliebiger PHP-Ausdruck sein:
{import $ajax ? 'ajax.latte' : 'not-ajax.latte'}
Sie können so viele {import}
Anweisungen in einer Vorlage verwenden, wie Sie wollen. Wenn zwei importierte
Vorlagen denselben Block definieren, gewinnt die erste Vorlage. Die höchste Priorität wird jedoch der Hauptvorlage eingeräumt,
die jeden importierten Block überschreiben kann.
Der Inhalt von überschriebenen Blöcken kann beibehalten werden, indem der Block wie ein übergeordneter Block eingefügt wird:
{layout 'layout.latte'}
{import 'blocks.latte'}
{block sidebar}
{include parent}
{/block}
{block title}...{/block}
{block content}...{/block}
In diesem Beispiel ruft {include parent}
den Block sidebar
korrekt aus der Vorlage
blocks.latte
auf.
Einheit Vererbung {embed}
Die Unit-Vererbung überträgt die Idee der Layout-Vererbung auf die Ebene der Inhaltsfragmente. Während die Layoutvererbung mit “Dokumentenskeletten” arbeitet, die durch untergeordnete Vorlagen zum Leben erweckt werden, können Sie mit der Unit-Vererbung Skelette für kleinere Inhaltseinheiten erstellen und diese an beliebiger Stelle wiederverwenden.
Bei der Vererbung von Einheiten ist das Tag {embed}
der Schlüssel. Es kombiniert das Verhalten von
{include}
und {layout}
. Es erlaubt Ihnen, den Inhalt einer anderen Vorlage oder eines Blocks einzubinden
und optional Variablen zu übergeben, genau wie {include}
. Außerdem können Sie damit jeden Block, der innerhalb der
eingebundenen Vorlage definiert ist, überschreiben, wie es {layout}
tut.
Als Beispiel werden wir das klappbare Akkordeon-Element verwenden. Werfen wir einen Blick auf das Element-Skelett in der
Vorlage collapsible.latte
:
<section class="collapsible {$modifierClass}">
<h4 class="collapsible__title">
{block title}{/block}
</h4>
<div class="collapsible__content">
{block content}{/block}
</div>
</section>
Die {block}
Tags definieren zwei Blöcke, die von untergeordneten Vorlagen ausgefüllt werden können. Ja, wie im
Fall der Elternvorlage in der Layout-Vererbungsvorlage. Sie sehen auch die Variable $modifierClass
.
Lassen Sie uns unser Element in der Vorlage verwenden. An dieser Stelle kommt {embed}
ins Spiel. Es ist ein sehr
leistungsfähiges Tool, mit dem wir alles machen können: den Vorlageninhalt des Elements einbinden, Variablen hinzufügen und
Blöcke mit benutzerdefiniertem HTML einfügen:
{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}
Die Ausgabe könnte wie folgt aussehen:
<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>
Blöcke innerhalb von Einbettungs-Tags bilden eine eigene, von anderen Blöcken unabhängige Ebene. Daher können sie den
gleichen Namen wie der Block außerhalb der Einbettung haben und werden in keiner Weise beeinflusst. Mit dem Tag include innerhalb von {embed}
Tags können Sie hier erstellte Blöcke, Blöcke aus
der eingebetteten Vorlage (die nicht lokal sind) und auch Blöcke aus der Hauptvorlage, die
lokal sind, einfügen. Sie können auch Blöcke aus anderen Dateien importieren:
{block outer}…{/block}
{block local hello}…{/block}
{embed 'collapsible.latte', modifierClass: my-style}
{import 'blocks.latte'}
{block inner}…{/block}
{block title}
{include inner} {* funktioniert, Block ist innerhalb von embed definiert *}
{include hello} {* funktioniert, Block ist lokal in dieser Vorlage *}
{include content} {* funktioniert, Block ist in der eingebetteten Vorlage definiert *}
{include aBlockDefinedInImportedTemplate} {* funktioniert *}
{include outer} {* funktioniert nicht! - Block befindet sich in äußerer Schicht *}
{/block}
{/embed}
Eingebettete Vorlagen haben keinen Zugriff auf die Variablen des aktiven Kontexts, aber sie haben Zugriff auf die globalen Variablen.
Mit {embed}
können Sie nicht nur Vorlagen, sondern auch andere Blöcke einfügen, so dass das vorherige Beispiel
wie folgt geschrieben werden könnte:
{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}
Wenn wir einen Ausdruck an {embed}
übergeben und es nicht klar ist, ob es sich um einen Block oder einen
Dateinamen handelt, fügen Sie das Schlüsselwort block
oder file
hinzu:
{embed block $name} ... {/embed}
Anwendungsfälle
Es gibt verschiedene Arten der Vererbung und der Wiederverwendung von Code in Latte. Lassen Sie uns die wichtigsten Konzepte zusammenfassen, um mehr Klarheit zu schaffen:
{include template}
Nutzungsfall: Verwendung von header.latte
& footer.latte
innerhalb von
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}
Benutzungsfall: Erweitern von layout.latte
innerhalb von homepage.latte
&
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}
Nutzungsfall: sidebar.latte
in 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}
Verwendungsfall: Eine Funktion, die einige Variablen abruft und ein Markup ausgibt.
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}
Anwendungsfall: Einbettung von pagination.latte
in product.table.latte
&
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}