Chapter 6: Twig Templates#
HTML Without the String Concatenation Hell#
You have built REST calls. You have populated MemTables. You have rendered HTML in your Delphi forms. But every time you construct HTML, you end up with code like this:
HTML := '<div class="card">' + '<h2>' + Customer.Name + '</h2>' + '<p>' + Customer.Email + '</p>' + '</div>';Six lines. Two bugs waiting to happen. One missing quote away from a broken layout. And when the designer changes the card to include a phone number, you are back in the Pascal editor, escaping quotes and hoping the HTML is still valid.
Twig templates fix this. Write HTML in HTML files. Drop in variables with {{ name }}. Add loops with {% for %}. Inherit layouts with {% extends %}. The template engine handles the rest. Your Pascal code passes data in; the template decides how to display it.
TTina4Twig is a Twig-compatible engine built directly into the Tina4 Delphi component library. It supports variables, control structures, filters, functions, template inheritance, macros, and integration with the HTML renderer. Every feature documented here works at design time and runtime.
1. TTina4Twig Standalone Usage#
The simplest way to use Twig is standalone -- create an instance, set variables, render a template string or file.
Create and Render#
uses Tina4Twig;โprocedure TForm1.RenderGreeting;var Twig: TTina4Twig; Variables: TStringDict;begin Twig := TTina4Twig.Create('C:\MyApp\templates'); Variables := TStringDict.Create; try Variables.Add('name', 'Andre'); Variables.Add('role', 'Developer');โ Memo1.Lines.Text := Twig.Render('greeting.html', Variables); finally Variables.Free; Twig.Free; end;end;The constructor takes a template path. This is the root directory where Twig looks for template files referenced by {% include %} and {% extends %} tags. Every file reference is relative to this path.
Render from a String#
You can also render inline template strings without loading from a file:
var Twig: TTina4Twig; Variables: TStringDict;begin Twig := TTina4Twig.Create(''); Variables := TStringDict.Create; try Variables.Add('name', 'World');โ Memo1.Lines.Text := Twig.Render( '<h1>Hello {{ name }}!</h1>', Variables); // Output: <h1>Hello World!</h1> finally Variables.Free; Twig.Free; end;end;Passing Complex Data#
Variables can be strings, numbers, arrays, or nested objects:
Variables.Add('name', 'Andre');Variables.Add('score', TValue.From<Integer>(42));Variables.Add('items', TValue.From<TArray<String>>(['Apple', 'Banana', 'Cherry']));2. Variables#
Variables are the bridge between your Pascal code and your templates. Everything you pass via SetVariable or through a TStringDict is accessible inside double curly braces.
Simple Variables#
Hello {{ name }}!Your score is {{ score }}.Nested Properties#
Access object properties with dot notation:
{{ user.email }}{{ order.customer.name }}{{ settings.theme.primaryColor }}Setting Variables Inside Templates#
Use {% set %} to define variables directly in a template:
{% set greeting = 'Hello' %}{% set items = ['Apple', 'Banana'] %}{% set total = price * quantity %}{% set fullName = firstName ~ ' ' ~ lastName %}โ<p>{{ greeting }}, {{ fullName }}!</p><p>Total: {{ total }}</p>The ~ operator concatenates strings. Variables set with {% set %} are scoped to the current template block.
3. Control Structures#
if / elseif / else#
Conditional rendering. Test any expression -- variable truthiness, comparisons, filters:
{% if users|length > 0 %} <ul> {% for user in users %} <li>{{ user.name }}</li> {% endfor %} </ul>{% elseif guests|length > 0 %} <p>Guests only today.</p>{% else %} <p>No users found.</p>{% endif %}Combine conditions with and, or, not:
{% if user.isAdmin and user.isActive %} <a href="#admin">Admin Panel</a>{% endif %}โ{% if not user.isVerified %} <div class="alert alert-warning">Please verify your email.</div>{% endif %}for Loops#
Iterate over arrays:
{% for item in items %} <p>{{ item }}</p>{% endfor %}Key-value pairs:
{% for key, value in settings %} <tr> <td>{{ key }}</td> <td>{{ value }}</td> </tr>{% endfor %}Ranges:
{% for i in 0..10 %} <option value="{{ i }}">{{ i }}</option>{% endfor %}โ{% for letter in 'a'..'f' %} <span>{{ letter }}</span>{% endfor %}The loop variable is available inside every for loop:
{% for item in items %} <tr class="{% if loop.first %}first{% endif %}{% if loop.last %}last{% endif %}"> <td>{{ loop.index }}</td> <td>{{ item.name }}</td> </tr>{% endfor %}with Blocks#
Scope variables to a block without polluting the outer context:
{% with { title: 'Dashboard', subtitle: 'Overview' } %} <div class="header"> <h1>{{ title }}</h1> <p>{{ subtitle }}</p> </div>{% endwith %}4. Template Inheritance#
This is where Twig saves you from copy-pasting the same header and footer into 30 pages.
Base Template (base.html)#
<!DOCTYPE html><html><head> <title>{% block title %}My App{% endblock %}</title> <style> body { font-family: Arial, sans-serif; margin: 0; padding: 20px; } .header { background: #2c3e50; color: white; padding: 15px; } .content { padding: 20px; } .footer { border-top: 1px solid #ccc; padding: 10px; color: #666; } </style> {% block head %}{% endblock %}</head><body> <div class="header"> <h1>{% block header %}My Application{% endblock %}</h1> </div> <div class="content"> {% block content %}{% endblock %} </div> <div class="footer"> {% block footer %}© 2026 My Company{% endblock %} </div></body></html>Child Template (dashboard.html)#
{% extends 'base.html' %} {% block title %}Dashboard - My App{% endblock %} {% block header %}Dashboard{% endblock %} {% block content %} <h2>Welcome, {{ userName }}</h2> <p>You have {{ messageCount }} new messages.</p>{% endblock %}The child template only defines the blocks it wants to override. Everything else comes from the base. Change the footer in base.html and every page that extends it picks up the change.
include#
Pull in reusable fragments:
{% include 'header.html' %}โ<div class="main"> {% include 'sidebar.html' with { menu: menuItems } %} <div class="content"> {{ content }} </div></div>โ{% include 'footer.html' %}The with clause passes variables to the included template. Without it, the included template inherits the parent's variables.
Setting the Template Path in Pascal#
For {% extends %} and {% include %} to resolve correctly, set the template path:
Twig := TTina4Twig.Create('C:\MyApp\templates');All template references are relative to this path. If dashboard.html says {% extends 'base.html' %}, Twig looks for C:\MyApp\templates\base.html.
5. Macros#
Macros are reusable template fragments -- the Twig equivalent of functions. Define them once, call them everywhere.
Define a Macro#
{% macro input(name, value, type) %} <div class="form-group"> <label for="{{ name }}">{{ name|capitalize }}</label> <input type="{{ type|default('text') }}" id="{{ name }}" name="{{ name }}" value="{{ value }}" class="form-control"> </div>{% endmacro %}Call a Macro#
{{ input('username', '', 'text') }}{{ input('email', user.email, 'email') }}{{ input('password', '', 'password') }}A More Complex Macro#
{% macro alert(message, type) %} <div class="alert alert-{{ type|default('info') }}" role="alert"> {% if type == 'danger' %} <strong>Error!</strong> {% elseif type == 'warning' %} <strong>Warning!</strong> {% endif %} {{ message }} </div>{% endmacro %}โ{{ alert('Record saved successfully.', 'success') }}{{ alert('Please check your input.', 'warning') }}{{ alert('Connection failed.', 'danger') }}6. Filters Reference#
Filters transform output values. Chain them with the pipe | operator:
{{ name|upper|length }}{{ description|striptags|trim|truncate(100) }}String Filters#
| Filter | What it does | Example |
|---|
| upper | Uppercase | {{ 'hello'\|upper }} produces HELLO | | lower | Lowercase | {{ 'HELLO'\|lower }} produces hello | | capitalize | First letter uppercase | {{ 'hello world'\|capitalize }} produces Hello world | | title | Title case every word | {{ 'hello world'\|title }} produces Hello World | | trim | Strip leading/trailing whitespace | {{ ' hi '\|trim }} produces hi | | nl2br | Convert newlines to <br> | {{ "line1\nline2"\|nl2br }} | | striptags | Remove HTML tags | {{ '<b>bold</b>'\|striptags }} produces bold | | replace | Replace substrings | {{ 'hello'\|replace({'e': 'a'}) }} produces hallo | | split | Split into array | {{ 'a,b,c'\|split(',') }} produces ['a','b','c'] | | slug | URL-friendly slug | {{ 'Hello World!'\|slug }} produces hello-world | | spaceless | Remove whitespace between HTML tags | {{ '<p> hi </p>'\|spaceless }} | | u | Unicode string wrapper | {{ text\|u }} |
Practical example -- building a navigation menu:
{% for page in pages %} <a href="/{{ page.title|slug }}" class="nav-link {% if page.title == currentPage %}active{% endif %}"> {{ page.title|title }} </a>{% endfor %}Number Filters#
| Filter | What it does | Example |
|---|
| abs | Absolute value | {{ -42\|abs }} produces 42 | | number_format | Format with decimals, separators | {{ 1234.5\|number_format(2, '.', ',') }} produces 1,234.50 | | format_number | Locale-aware number format | {{ 1234\|format_number }} | | format_currency | Format as currency | {{ 1234.50\|format_currency('USD') }} produces $1,234.50 |
Practical example -- product pricing:
{% for product in products %} <div class="product-card"> <h3>{{ product.name }}</h3> <p class="price">{{ product.price|format_currency('USD') }}</p> {% if product.discount > 0 %} <p class="discount">Save {{ product.discount|abs }}%</p> {% endif %} </div>{% endfor %}Array Filters#
| Filter | What it does | Example |
|---|
| length | Count elements | {{ items\|length }} | | first | First element | {{ items\|first }} | | last | Last element | {{ items\|last }} | | join | Join into string | {{ items\|join(', ') }} | | keys | Get object keys | {{ settings\|keys }} | | merge | Merge two arrays | {{ defaults\|merge(overrides) }} | | sort | Sort ascending | {{ items\|sort }} | | reverse | Reverse order | {{ items\|reverse }} | | shuffle | Random order | {{ items\|shuffle }} | | slice | Extract subset | {{ items\|slice(0, 5) }} -- first 5 items | | batch | Split into chunks | {{ items\|batch(3) }} -- groups of 3 | | column | Extract one property | {{ users\|column('name') }} | | filter | Keep matching items | {{ items\|filter }} | | find | Find first match | {{ items\|find }} | | map | Transform each item | {{ items\|map }} | | reduce | Reduce to single value | {{ items\|reduce }} | | min | Smallest value | {{ prices\|min }} | | max | Largest value | {{ prices\|max }} |
Practical example -- rendering a product grid:
{% for row in products|batch(3) %} <div class="row"> {% for product in row %} <div class="col"> <h4>{{ product.name }}</h4> <p>{{ product.price|number_format(2) }}</p> </div> {% endfor %} </div>{% endfor %}โ<p>Showing {{ products|length }} products. Price range: {{ products|column('price')|min|format_currency('USD') }} to {{ products|column('price')|max|format_currency('USD') }}</p>โ<p>Categories: {{ products|column('category')|sort|join(', ') }}</p>Date Filters#
| Filter | What it does | Example |
|---|
| date | Format a date | {{ order.created\|date('Y-m-d') }} | | date_modify | Shift a date | {{ order.created\|date_modify('+30 days') }} | | format_date | Format date (alias) | {{ order.created\|format_date }} | | format_datetime | Format date and time | {{ order.created\|format_datetime }} | | format_time | Format time only | {{ order.created\|format_time }} |
Date format specifiers (PHP-style, auto-converted by Tina4):
| Specifier | Meaning | Output |
|---|---|---|
Y | 4-digit year | 2026 |
y | 2-digit year | 26 |
m | Month, zero-padded | 03 |
n | Month, no padding | 3 |
d | Day, zero-padded | 01 |
j | Day, no padding | 1 |
H | Hour 24h, zero-padded | 14 |
h | Hour 12h, zero-padded | 02 |
i | Minutes | 30 |
s | Seconds | 05 |
A | AM/PM | PM |
D | Short day name | Mon |
l | Full day name | Monday |
M | Short month name | Jan |
F | Full month name | January |
Practical example -- order timeline:
<h3>Order #{{ order.id }}</h3><table> <tr> <td>Placed:</td> <td>{{ order.created|date('F j, Y') }} at {{ order.created|date('h:i A') }}</td> </tr> <tr> <td>Ships by:</td> <td>{{ order.created|date_modify('+3 days')|date('F j, Y') }}</td> </tr> <tr> <td>Delivery estimate:</td> <td>{{ order.created|date_modify('+7 days')|date('l, F j') }}</td> </tr></table>Encoding Filters#
| Filter | What it does | Example |
|---|
| escape / e | Escape HTML entities | {{ userInput\|escape }} | | raw | Output without escaping | {{ trustedHtml\|raw }} | | url_encode | URL-encode a string | {{ query\|url_encode }} | | json_encode | Encode as JSON | {{ data\|json_encode }} | | json_decode | Decode from JSON | {{ jsonString\|json_decode }} | | convert_encoding | Convert character encoding | {{ text\|convert_encoding('UTF-8') }} | | data_uri | Create a data URI | {{ imageData\|data_uri }} |
Practical example -- building a search URL:
<a href="/search?q={{ searchTerm|url_encode }}&category={{ category|url_encode }}"> Search for "{{ searchTerm|escape }}"</a>โ<script> var config = {{ appSettings|json_encode|raw }};</script>Other Filters#
| Filter | What it does | Example |
|---|
| default | Fallback value if empty/null | {{ name\|default('Guest') }} | | format | Printf-style formatting | {{ 'Hello %s, you have %d items'\|format(name, count) }} | | plural | Pluralize a word | {{ 'item'\|plural }} produces items | | singular | Singularize a word | {{ 'items'\|singular }} produces item |
<p>You have {{ count }} {{ 'item'|plural }} in your cart.</p><p>Welcome, {{ userName|default('Guest') }}!</p><p>{{ 'Found %d %s in %s'|format(results|length, 'result'|plural, category) }}</p>7. Functions#
Functions work like filters but are called directly:
| Function | What it does | Example |
|---|
| range() | Generate a sequence | {% for i in range(1, 10) %} | | dump() | Debug output of a variable | {{ dump(user) }} | | date() | Create or format a date | {{ date('now', 'Y-m-d') }} |
{# Generate page numbers #}{% for page in range(1, totalPages) %} <a href="?page={{ page }}" class="{% if page == currentPage %}active{% endif %}"> {{ page }} </a>{% endfor %}โ{# Debug during development #}{% if debug %} <pre>{{ dump(order) }}</pre>{% endif %}โ{# Show current date #}<p>Report generated: {{ date('now', 'F j, Y \\a\\t h:i A') }}</p>8. Operators#
| Category | Operators | Example |
|---|
| Comparison | ==, !=, <, >, <=, >= | {% if price > 100 %} | | Logical | and, or, not | {% if active and verified %} | | String | ~ (concat), in, starts with, ends with, matches | {{ first ~ ' ' ~ last }} | | Math | +, -, *, /, %, ** | {{ price * quantity }} | | Range | .. | {% for i in 1..10 %} |
String operators in action:
{# Concatenation #}{% set fullName = firstName ~ ' ' ~ lastName %}โ{# Membership test #}{% if 'admin' in user.roles %} <a href="#admin">Admin Panel</a>{% endif %}โ{# String matching #}{% if email ends with '@company.com' %} <span class="badge">Internal</span>{% endif %}โ{% if filename starts with 'report_' %} <span class="badge">Report</span>{% endif %}โ{% if phone matches '/^\\+27/' %} <span>South Africa</span>{% endif %}9. Integration with TTina4HTMLRender#
The HTML renderer has built-in Twig support. You do not need to create a standalone TTina4Twig instance -- the renderer handles it internally.
Setting Variables and Rendering#
// Set variables before assigning the templateTina4HTMLRender1.SetTwigVariable('title', 'Dashboard');Tina4HTMLRender1.SetTwigVariable('userName', 'Andre');Tina4HTMLRender1.SetTwigVariable('messageCount', '5');โ// Set the Twig template -- it renders to HTML automaticallyTina4HTMLRender1.Twig.Text := '<div class="header">' + ' <h1>{{ title }}</h1>' + ' <p>Welcome back, {{ userName }}! You have {{ messageCount }} new messages.</p>' + '</div>';File-Based Templates with the Renderer#
// Set the template path for includes and extendsTina4HTMLRender1.TwigTemplatePath := 'C:\MyApp\templates';โ// Set variablesTina4HTMLRender1.SetTwigVariable('userName', 'Andre');Tina4HTMLRender1.SetTwigVariable('notifications', '3');โ// Load from file -- Twig processes it, then the renderer displays the HTMLTina4HTMLRender1.Twig.LoadFromFile('C:\MyApp\templates\dashboard.html');Combining with REST Data#
The real power emerges when you combine REST data with Twig templates:
procedure TForm1.LoadCustomerCard(CustomerID: Integer);var StatusCode: Integer; Response: TJSONObject;begin Response := Tina4REST1.Get(StatusCode, '/customers/' + CustomerID.ToString); try Tina4HTMLRender1.SetTwigVariable('customer', Response.GetValue<String>('name')); Tina4HTMLRender1.SetTwigVariable('email', Response.GetValue<String>('email')); Tina4HTMLRender1.SetTwigVariable('orders', Response.GetValue<String>('orderCount'));โ Tina4HTMLRender1.Twig.LoadFromFile( 'C:\MyApp\templates\customer-card.html'); finally Response.Free; end;end;10. Complete Example: Email Template System#
Build a reusable email template system with a base layout, customer notification, and order confirmation.
File Structure#
templates/ email/ base.html -- shared email layout notification.html -- customer notification order-confirm.html -- order confirmation with product loopbase.html#
<!DOCTYPE html><html><head> <style> body { font-family: Arial, sans-serif; background: #f4f4f4; margin: 0; padding: 0; } .container { max-width: 600px; margin: 20px auto; background: white; border-radius: 8px; overflow: hidden; } .header { background: #2c3e50; color: white; padding: 20px; text-align: center; } .content { padding: 30px; } .footer { background: #ecf0f1; padding: 15px; text-align: center; font-size: 12px; color: #666; } .btn { display: inline-block; padding: 10px 20px; background: #1abc9c; color: white; text-decoration: none; border-radius: 4px; } </style></head><body> <div class="container"> <div class="header"> <h1>{% block header %}{{ companyName|default('My Company') }}{% endblock %}</h1> </div> <div class="content"> {% block content %}{% endblock %} </div> <div class="footer"> {% block footer %} © {{ date('now', 'Y') }} {{ companyName|default('My Company') }}. All rights reserved. {% endblock %} </div> </div></body></html>notification.html#
{% extends 'email/base.html' %} {% block header %}Notification{% endblock %} {% block content %} <h2>Hello {{ customerName|default('Customer') }},</h2> <p>{{ message }}</p> {% if actionUrl %} <p style="text-align: center; margin: 30px 0;"> <a href="{{ actionUrl }}" class="btn">{{ actionText|default('View Details') }}</a> </p> {% endif %} {% if notes|length > 0 %} <h3>Additional Notes:</h3> <ul> {% for note in notes %} <li>{{ note }}</li> {% endfor %} </ul> {% endif %}{% endblock %}order-confirm.html#
{% extends 'email/base.html' %} {% block header %}Order Confirmation{% endblock %} {% block content %} <h2>Thank you, {{ customerName }}!</h2> <p>Your order <strong>#{{ orderId }}</strong> has been confirmed.</p> <p>Placed on: {{ orderDate|date('F j, Y') }}</p> <table style="width: 100%; border-collapse: collapse; margin: 20px 0;"> <thead> <tr style="background: #ecf0f1;"> <th style="padding: 10px; text-align: left;">Product</th> <th style="padding: 10px; text-align: right;">Qty</th> <th style="padding: 10px; text-align: right;">Price</th> <th style="padding: 10px; text-align: right;">Total</th> </tr> </thead> <tbody> {% for item in items %} <tr style="border-bottom: 1px solid #eee;"> <td style="padding: 10px;">{{ item.name }}</td> <td style="padding: 10px; text-align: right;">{{ item.quantity }}</td> <td style="padding: 10px; text-align: right;">{{ item.price|format_currency('USD') }}</td> <td style="padding: 10px; text-align: right;">{{ item.total|format_currency('USD') }}</td> </tr> {% endfor %} </tbody> <tfoot> <tr> <td colspan="3" style="padding: 10px; text-align: right;"><strong>Subtotal:</strong></td> <td style="padding: 10px; text-align: right;">{{ subtotal|format_currency('USD') }}</td> </tr> <tr> <td colspan="3" style="padding: 10px; text-align: right;">Tax ({{ taxRate }}%):</td> <td style="padding: 10px; text-align: right;">{{ tax|format_currency('USD') }}</td> </tr> {% if discount > 0 %} <tr style="color: #e74c3c;"> <td colspan="3" style="padding: 10px; text-align: right;">Discount:</td> <td style="padding: 10px; text-align: right;">-{{ discount|format_currency('USD') }}</td> </tr> {% endif %} <tr style="font-size: 1.2em; font-weight: bold;"> <td colspan="3" style="padding: 10px; text-align: right;">Total:</td> <td style="padding: 10px; text-align: right;">{{ grandTotal|format_currency('USD') }}</td> </tr> </tfoot> </table> <p>Estimated delivery: {{ orderDate|date_modify('+5 days')|date('l, F j, Y') }}</p>{% endblock %}Pascal Code to Render#
procedure TForm1.SendOrderConfirmation(OrderID: Integer);var Twig: TTina4Twig; Variables: TStringDict; EmailBody: String;begin Twig := TTina4Twig.Create('C:\MyApp\templates'); Variables := TStringDict.Create; try Variables.Add('companyName', 'Acme Store'); Variables.Add('customerName', 'Andre van Zuydam'); Variables.Add('orderId', OrderID.ToString); Variables.Add('orderDate', FormatDateTime('yyyy-mm-dd', Now)); Variables.Add('subtotal', '149.97'); Variables.Add('taxRate', '15'); Variables.Add('tax', '22.50'); Variables.Add('discount', '10.00'); Variables.Add('grandTotal', '162.47');โ // Items would be passed as a JSON array string or structured data Variables.Add('items', '[' + '{"name": "Widget Pro", "quantity": "2", "price": "49.99", "total": "99.98"},' + '{"name": "Gadget Mini", "quantity": "1", "price": "49.99", "total": "49.99"}' + ']');โ EmailBody := Twig.Render('email/order-confirm.html', Variables); Memo1.Lines.Text := EmailBody; finally Variables.Free; Twig.Free; end;end;11. Complete Example: Report Generator#
Build a product catalog report with categories, pricing, inventory status, filters, and macros.
report-catalog.html#
{% macro statusBadge(quantity) %} {% if quantity > 10 %} <span style="color: #27ae60; font-weight: bold;">In Stock ({{ quantity }})</span> {% elseif quantity > 0 %} <span style="color: #f39c12; font-weight: bold;">Low Stock ({{ quantity }})</span> {% else %} <span style="color: #e74c3c; font-weight: bold;">Out of Stock</span> {% endif %}{% endmacro %} {% macro priceCell(price, discount) %} {% if discount > 0 %} <span style="text-decoration: line-through; color: #999;">{{ price|format_currency('USD') }}</span> <span style="color: #e74c3c; font-weight: bold;"> {{ (price - discount)|format_currency('USD') }} </span> {% else %} {{ price|format_currency('USD') }} {% endif %}{% endmacro %} <!DOCTYPE html><html><head> <style> body { font-family: Arial, sans-serif; margin: 20px; } h1 { color: #2c3e50; } h2 { color: #34495e; border-bottom: 2px solid #1abc9c; padding-bottom: 5px; } table { width: 100%; border-collapse: collapse; margin-bottom: 30px; } th { background: #2c3e50; color: white; padding: 10px; text-align: left; } td { padding: 8px; border-bottom: 1px solid #eee; } tr:nth-child(even) { background: #f9f9f9; } .summary { background: #ecf0f1; padding: 15px; border-radius: 8px; margin: 20px 0; } </style></head><body> <h1>{{ reportTitle|default('Product Catalog') }}</h1> <p>Generated: {{ date('now', 'F j, Y \\a\\t h:i A') }}</p> <p>Total products: {{ products|length }}</p> <div class="summary"> <strong>Price Range:</strong> {{ products|column('price')|min|format_currency('USD') }} - {{ products|column('price')|max|format_currency('USD') }} </div> {% for category in categories %} <h2>{{ category.name|title }}</h2> {% set categoryProducts = products|filter %} <table> <thead> <tr> <th>#</th> <th>Product</th> <th>SKU</th> <th>Price</th> <th>Stock</th> </tr> </thead> <tbody> {% for product in category.products %} <tr> <td>{{ loop.index }}</td> <td> <strong>{{ product.name }}</strong> {% if product.description %} <br><small style="color: #666;">{{ product.description|striptags|slice(0, 80) }}...</small> {% endif %} </td> <td>{{ product.sku|upper }}</td> <td>{{ priceCell(product.price, product.discount|default(0)) }}</td> <td>{{ statusBadge(product.stock) }}</td> </tr> {% endfor %} </tbody> </table> {% endfor %} <div class="summary"> <h3>Report Summary</h3> <p>Categories: {{ categories|length }}</p> <p>Total Products: {{ products|length }}</p> <p>Products in stock: {{ products|column('stock')|filter|length }}</p> </div></body></html>Pascal Code#
procedure TForm1.GenerateCatalogReport;var Twig: TTina4Twig; Variables: TStringDict;begin Twig := TTina4Twig.Create('C:\MyApp\templates'); Variables := TStringDict.Create; try Variables.Add('reportTitle', 'Q1 2026 Product Catalog');โ // In a real app, this data comes from GetJSONFromDB or a REST call Variables.Add('categories', '[' + '{"name": "electronics", "products": [' + '{"name": "USB-C Hub", "sku": "elec-001", "price": "29.99", "stock": "45", "description": "7-port USB-C hub with HDMI"},' + '{"name": "Wireless Mouse", "sku": "elec-002", "price": "19.99", "stock": "3", "discount": "5.00"},' + '{"name": "Mechanical Keyboard", "sku": "elec-003", "price": "89.99", "stock": "0"}' + ']},' + '{"name": "office supplies", "products": [' + '{"name": "Notebook Set", "sku": "off-001", "price": "12.99", "stock": "120"},' + '{"name": "Pen Pack", "sku": "off-002", "price": "8.99", "stock": "200"}' + ']}' + ']');โ Variables.Add('products', '[' + '{"name": "USB-C Hub", "price": "29.99", "stock": "45"},' + '{"name": "Wireless Mouse", "price": "19.99", "stock": "3"},' + '{"name": "Mechanical Keyboard", "price": "89.99", "stock": "0"},' + '{"name": "Notebook Set", "price": "12.99", "stock": "120"},' + '{"name": "Pen Pack", "price": "8.99", "stock": "200"}' + ']');โ WebBrowser1.Navigate('about:blank'); // Render and display the report var HTML := Twig.Render('report-catalog.html', Variables); Memo1.Lines.Text := HTML; finally Variables.Free; Twig.Free; end;end;Exercise: Invoice Template#
Build an invoice template with the following requirements:
- Company header with logo placeholder, company name, and address
- Customer billing and shipping address blocks
- Line items loop with description, quantity, unit price, and line total
- Subtotal, tax (configurable rate), and grand total calculations
- Conditional discount display (only shows discount row if discount > 0)
- Payment terms section with due date (30 days from invoice date)
- Use macros for the address block (reuse for billing and shipping)
- Use template inheritance -- extend a base invoice layout
Solution#
invoice-base.html:
<!DOCTYPE html><html><head> <style> body { font-family: Arial, sans-serif; margin: 0; padding: 30px; color: #333; } .invoice-header { display: flex; justify-content: space-between; margin-bottom: 30px; } .company-info h1 { margin: 0; color: #2c3e50; } .invoice-meta { text-align: right; } .addresses { display: flex; gap: 40px; margin-bottom: 30px; } .address-block { flex: 1; } .address-block h3 { color: #2c3e50; margin-bottom: 5px; } table { width: 100%; border-collapse: collapse; } th { background: #2c3e50; color: white; padding: 10px; text-align: left; } td { padding: 8px 10px; border-bottom: 1px solid #eee; } .totals td { border: none; } .grand-total { font-size: 1.3em; font-weight: bold; color: #2c3e50; } .terms { margin-top: 30px; padding: 15px; background: #f9f9f9; border-radius: 4px; } </style> {% block head %}{% endblock %}</head><body> {% block invoice %}{% endblock %}</body></html>invoice.html:
{% extends 'invoice-base.html' %} {% macro addressBlock(label, addr) %} <div class="address-block"> <h3>{{ label }}</h3> <p> <strong>{{ addr.name }}</strong><br> {{ addr.street }}<br> {{ addr.city }}, {{ addr.state }} {{ addr.zip }}<br> {% if addr.country %}{{ addr.country }}<br>{% endif %} {% if addr.phone %}Tel: {{ addr.phone }}{% endif %} </p> </div>{% endmacro %} {% block invoice %} <div class="invoice-header"> <div class="company-info"> <h1>{{ company.name }}</h1> <p>{{ company.address }}<br> {{ company.city }}, {{ company.state }} {{ company.zip }}<br> {{ company.email }}</p> </div> <div class="invoice-meta"> <h2>INVOICE</h2> <p> <strong>Invoice #:</strong> {{ invoiceNumber }}<br> <strong>Date:</strong> {{ invoiceDate|date('F j, Y') }}<br> <strong>Due Date:</strong> {{ invoiceDate|date_modify('+30 days')|date('F j, Y') }} </p> </div> </div> <div class="addresses"> {{ addressBlock('Bill To', billing) }} {{ addressBlock('Ship To', shipping) }} </div> <table> <thead> <tr> <th style="width: 50%;">Description</th> <th style="text-align: right;">Qty</th> <th style="text-align: right;">Unit Price</th> <th style="text-align: right;">Total</th> </tr> </thead> <tbody> {% for item in lineItems %} <tr> <td>{{ item.description }}</td> <td style="text-align: right;">{{ item.quantity }}</td> <td style="text-align: right;">{{ item.unitPrice|format_currency('USD') }}</td> <td style="text-align: right;">{{ item.lineTotal|format_currency('USD') }}</td> </tr> {% endfor %} </tbody> <tfoot> <tr class="totals"> <td colspan="3" style="text-align: right; padding-top: 15px;"><strong>Subtotal:</strong></td> <td style="text-align: right; padding-top: 15px;">{{ subtotal|format_currency('USD') }}</td> </tr> {% if discount > 0 %} <tr class="totals" style="color: #27ae60;"> <td colspan="3" style="text-align: right;">Discount ({{ discountPercent }}%):</td> <td style="text-align: right;">-{{ discount|format_currency('USD') }}</td> </tr> {% endif %} <tr class="totals"> <td colspan="3" style="text-align: right;">Tax ({{ taxRate }}%):</td> <td style="text-align: right;">{{ tax|format_currency('USD') }}</td> </tr> <tr class="totals grand-total"> <td colspan="3" style="text-align: right;">Total Due:</td> <td style="text-align: right;">{{ grandTotal|format_currency('USD') }}</td> </tr> </tfoot> </table> <div class="terms"> <h3>Payment Terms</h3> <p>Payment is due within 30 days of the invoice date. Please reference invoice #{{ invoiceNumber }} with your payment.</p> <p>Bank: {{ company.bank|default('First National Bank') }}<br> Account: {{ company.account|default('Contact us for details') }}</p> </div>{% endblock %}Pascal code to render the invoice:
procedure TForm1.GenerateInvoice;var Twig: TTina4Twig; Variables: TStringDict;begin Twig := TTina4Twig.Create('C:\MyApp\templates'); Variables := TStringDict.Create; try Variables.Add('invoiceNumber', 'INV-2026-0042'); Variables.Add('invoiceDate', FormatDateTime('yyyy-mm-dd', Now));โ Variables.Add('company', '{"name": "Acme Corp", "address": "123 Main St", ' + '"city": "Cape Town", "state": "WC", "zip": "8001", ' + '"email": "billing@acme.co.za"}');โ Variables.Add('billing', '{"name": "John Smith", "street": "456 Oak Ave", ' + '"city": "Johannesburg", "state": "GP", "zip": "2001", "phone": "+27 11 555 0123"}');โ Variables.Add('shipping', '{"name": "John Smith", "street": "789 Pine Rd", ' + '"city": "Durban", "state": "KZN", "zip": "4001"}');โ Variables.Add('lineItems', '[' + '{"description": "Widget Pro - Annual License", "quantity": "5", "unitPrice": "99.00", "lineTotal": "495.00"},' + '{"description": "Setup & Configuration", "quantity": "1", "unitPrice": "250.00", "lineTotal": "250.00"},' + '{"description": "Training (per hour)", "quantity": "4", "unitPrice": "75.00", "lineTotal": "300.00"}' + ']');โ Variables.Add('subtotal', '1045.00'); Variables.Add('discountPercent', '10'); Variables.Add('discount', '104.50'); Variables.Add('taxRate', '15'); Variables.Add('tax', '141.08'); Variables.Add('grandTotal', '1081.58');โ Memo1.Lines.Text := Twig.Render('invoice.html', Variables); finally Variables.Free; Twig.Free; end;end;Common Gotchas#
Template path resolution. Every {% extends %} and {% include %} path is relative to the path you pass to TTina4Twig.Create(). If you pass an empty string, file-based template references will fail silently. Always set a real path:
// Wrong -- includes and extends will not find filesTwig := TTina4Twig.Create('');โ// RightTwig := TTina4Twig.Create('C:\MyApp\templates');Variable scope in for loops. Variables set inside a {% for %} block do not persist outside the loop. This trips people up when trying to accumulate a total:
{# This does NOT work as expected #}{% set total = 0 %}{% for item in items %} {% set total = total + item.price %}{% endfor %}<p>Total: {{ total }}</p> {# Still 0! #}Calculate totals in your Pascal code and pass them as variables instead.
HTML escaping by default. Twig escapes HTML entities by default. If you pass pre-formatted HTML as a variable, it will show raw tags. Use the raw filter for trusted content:
{# Variable contains '<strong>Bold</strong>' #}{{ content }} {# Shows: <strong>Bold</strong> #}{{ content|raw }} {# Shows: Bold (rendered as HTML) #}Only use raw on content you control. Never use it on user input.
TStringDict memory management. Always free the TStringDict and TTina4Twig in a try/finally block. Forgetting this is the most common memory leak with Twig templates:
Twig := TTina4Twig.Create('C:\templates');Variables := TStringDict.Create;try // ... render ...finally Variables.Free; // Always free both Twig.Free;end;Whitespace in templates. Twig preserves whitespace exactly as written in the template. If you see extra blank lines in your output, your template has extra blank lines. Use {%- and -%} to trim whitespace around tags:
{%- for item in items -%} <li>{{ item }}</li>{%- endfor -%}