Template-Syntax

Variablen, Schleifen und Bedingungen in einer Tuix PDF-Vorlage.

Vorlagen werden mit Liquid gerendert. Das Objekt data einer Anfrage liefert die Variablen, jede sprichst du mit ihrem blossen Namen an.

Variablen

<h1>Rechnung für {{ customerName }}</h1>
{ "customerName": "Jane Doe" }

Verschachtelte Objekte sprichst du mit Punkten an:

<p>{{ customer.name }}, {{ customer.city }}</p>
{ "customer": { "name": "Jane Doe", "city": "Zürich" } }

Kein führender Punkt. {{ .customerName }} ist kein gültiges Liquid: Die Vorlage lässt sich nicht parsen und die Anfrage antwortet mit 500. Wenn du eine Vorlage aus der früheren Go-Syntax migrierst, entferne den Punkt aus jedem Platzhalter.

Fehlende Werte

Eine Variable, die nicht in data steht, wird als leerer String gerendert, die Anfrage bleibt erfolgreich. Dasselbe gilt für null. Kapsle optionale Blöcke explizit, wenn eine leere Zeile stören würde:

{% if discount %}<p>Rabatt: {{ discount }}</p>{% endif %}

Bedingungen

{% if paid %}
    <span class="badge">BEZAHLT</span>
{% else %}
    <span class="badge">OFFEN</span>
{% endif %}

Nur null und false sind falsch. Anders als in den meisten Sprachen sind 0, "" und ein leeres Array in Liquid wahr, {% if count %} führt den Block also auch bei count gleich 0 aus. Vergleiche in diesen Fällen explizit: {% if count > 0 %} oder {% if name != "" %}.

Übrige Vergleiche verhalten sich wie erwartet, and und or verknüpfen sie:

{% if total > 100 and currency == "CHF" %}<p>Versandkostenfrei</p>{% endif %}

Schleifen

for iteriert über ein Array, die Schleifenvariable benennst du selbst:

<table>
    {% for item in items %}
    <tr>
        <td>{{ item.name }}</td>
        <td>{{ item.price }}</td>
    </tr>
    {% endfor %}
</table>
{
    "items": [
        { "name": "Beratung", "price": "200.00" },
        { "name": "Hosting", "price": "49.00" }
    ]
}

Variablen der obersten Ebene bleiben innerhalb der Schleife sichtbar, ein besonderes Präfix brauchst du nicht:

{% for item in items %}
<tr>
    <td>{{ item.name }}</td>
    <td>{{ currency }} {{ item.price }}</td>
</tr>
{% endfor %}

forloop.index ist die Position des aktuellen Eintrags beginnend bei 1, {{ items.size }} liefert die Anzahl der Einträge.

Filter

Ein Filter wandelt einen Wert mit | um, mehrere lassen sich verketten:

<p>{{ customerName | upcase }}</p>
<p>{{ description | truncate: 80 }}</p>
<p>{{ invoiceDate | date: "%d.%m.%Y" }}</p>

Die vollständige Liste steht in der Liquid-Filter-Referenz.

Zahlen formatieren

JSON-Zahlen werden ausgegeben, wie sie ankommen, 249.5 also als 249.5.

round: 2 begrenzt die Nachkommastellen, füllt sie aber nicht auf: 249.5 bleibt 249.5 und wird nicht zu 249.50. Es gibt keinen Filter, der zwei Nachkommastellen garantiert. Übergib Beträge, die ein festes Format haben sollen, deshalb als bereits formatierte Strings in data ("249.50").

HTML in Werten

Werte werden nicht escaped. Ein data-Wert "<b>hi</b>" erscheint als Fettschrift, und jedes Markup, das du übergibst, landet als Markup im PDF. Wende auf Werte aus nicht vertrauenswürdigen Quellen, die wörtlich erscheinen sollen, den Filter escape an:

<p>{{ userComment | escape }}</p>

Styling und Seitenformat

  • Gerendert wird als A4 im Hochformat mit 300 DPI.
  • Lege dein CSS in einen <style>-Block innerhalb der Vorlage.
  • Bilder und Schriften müssen über öffentliche URLs erreichbar sein (oder als Data-URI eingebettet), da der Renderer sie von ausserhalb deines Netzwerks lädt.
  • Prüfe das Layout mit der Live-Vorschau im Editor und Set variables, bevor du rendern lässt. Siehe Vorlage anlegen.

Seitenumbrüche

Inhalt, der nicht auf eine Seite passt, läuft auf der nächsten weiter. Wo der Umbruch liegt, bestimmst du mit page-break-before, page-break-after und page-break-inside. Der Renderer berücksichtigt alle drei, und die Vorschau im Editor zeigt jede Seite als eigenes Blatt.

Verwende diese page-break-*-Namen, nicht die modernen break-before / break-after / break-inside. Der Renderer basiert auf einem älteren WebKit: Er ignoriert die modernen Eigenschaften stillschweigend, das PDF kommt also ohne Fehlermeldung als eine lange Seite zurück.

Eine neue Seite beginnen

page-break-after: always bricht nach dem Element um, page-break-before: always davor:

<div class="intro" style="page-break-after: always;">
    <h1>Einleitung</h1>
    <p>Dieser Abschnitt füllt Seite 1 allein.</p>
</div>
<div class="rest">
    <h1>Details</h1>
    <p>Das beginnt auf Seite 2.</p>
</div>

page-break-before auf dem allerersten Element des Dokuments bewirkt nichts. Ein wiederholter Abschnitt kann die Eigenschaft deshalb tragen, ohne eine leere erste Seite zu erzeugen. Ein Kapitel pro Seite:

<style>
    .chapter { page-break-before: always; }
</style>
 
{% for chapter in chapters %}
<div class="chapter">
    <h1>{{ chapter.title }}</h1>
    <p>{{ chapter.body }}</p>
</div>
{% endfor %}

Einen Block zusammenhalten

page-break-inside: avoid verschiebt ein Element auf die nächste Seite, anstatt es am Seitenrand zu zerschneiden:

<style>
    .card { page-break-inside: avoid; }
</style>
 
<div class="card">
    <h2>{{ product.name }}</h2>
    <p>{{ product.description }}</p>
</div>

Ein Block, der höher als eine Seite ist, wird trotzdem umbrochen, denn es gibt keine Seite, auf die er ganz passt. Auf eine Seite passen im Massstab des Renderers ungefähr 1300 px Inhalt.

Eine Überschrift kann allein am Ende einer Seite stehen, während ihr Text erst auf der nächsten beginnt. Damit beide zusammenbleiben, fasse die Überschrift und ihren ersten Absatz in einen Block, der nicht umbrochen wird:

<div style="page-break-inside: avoid;">
    <h2>{{ section.title }}</h2>
    <p>{{ section.intro }}</p>
</div>

Tabellen über mehrere Seiten

Eine Tabelle über mehrere Seiten wiederholt ihren Kopf auf jeder Seite, wenn die Kopfzeilen in einem thead stehen. Zeilen werden nie geteilt: Eine Zeile, die nicht mehr passt, wandert ganz auf die nächste Seite, auch wenn sie mehrere Textzeilen enthält.

<table>
    <thead>
        <tr><th>Position</th><th>Betrag</th></tr>
    </thead>
    <tbody>
        {% for item in items %}
        <tr><td>{{ item.name }}</td><td>{{ item.price }}</td></tr>
        {% endfor %}
    </tbody>
</table>

Was nicht möglich ist

  • @page-Regeln. Ränder und Seitengrösse lassen sich nicht aus der Vorlage ändern. Jedes PDF ist A4 im Hochformat mit einem Rand von rund 10 mm pro Seite.
  • Kopf- und Fusszeilen sowie Seitenzahlen. Es gibt weder counter(page) noch @bottom-center, „Seite 1 von 3“ ist aus einer Vorlage also nicht machbar.
  • JavaScript. Es ist beim Rendern deaktiviert, die Paginierung kann also nicht per Skript berechnet werden.

Auf dieser Seite