Template Design
Learn how to create beautiful, dynamic document templates using the visual editor and data binding syntax.
Overview
Templates in Shablonix define the structure and styling of your generated documents. You can create templates using:
- Visual Editor - Drag-and-drop interface for designing templates without code
- HTML/CSS - Write templates directly in code for full control
- Hybrid - Use the visual editor and customize with code
Visual Editor
The visual editor provides a drag-and-drop interface for building templates. It includes pre-built components that you can drag onto the canvas and customize.
Available Components
Layout
- Container - Wrapper with padding/margins
- Grid - Multi-column layouts
- Flex - Flexible box layouts
- Spacer - Vertical spacing
Content
- Heading - H1 through H6
- Paragraph - Body text
- Image - Static or dynamic images
- Table - Data tables with bindings
Data
- Data Field - Single value binding
- Data List - Iterate over arrays
- Conditional - Show/hide content
Design
- Divider - Horizontal lines
- Box - Styled containers
- Badge - Status indicators
Editor Features
- + Real-time preview with sample data
- + AI-assisted section drafting and template change planning inside the editor
- + Autosave with restore history for the latest 10 checkpoints
- + Responsive design tools for different page sizes
- + Component styling panel with typography, colors, and spacing
- + Export to HTML/CSS for further customization
AI Assistance
Shablonix can assist with template building directly in the visual editor. Clients connect their own AI provider in dashboard settings, then use AI Assist inside the editor to plan structural changes or draft reusable sections such as sender/recipient blocks, personal details tables, totals summaries, or payment instructions.
- + Plan a template change before editing the layout
- + Draft reusable HTML/CSS sections that can be inserted into the current document
- + Save generated sections to the shared chunk library for reuse across templates
Autosave & History
The editor keeps your working draft up to date automatically and preserves a rolling history of the latest 10 snapshots. This gives teams Google Docs-style safety without turning every keystroke into a permanent version.
- + Autosave keeps the current draft current in the background
- + Manual snapshots create explicit restore points before risky edits
- + AI insertions and restore actions create safety checkpoints automatically
- + Restoring a checkpoint creates a new head state instead of deleting history
Data Binding Syntax
Use double curly braces {{...}}
to insert dynamic data into any text field. Paths use dot notation to traverse nested
objects.
Basic Data Paths
<!-- Simple path -->
<p>Hello, {{customer.name}}!</p>
<!-- Output: Hello, Veronica Costello! -->
<!-- Nested properties -->
<p>{{customer.address.city}}, {{customer.address.country}}</p>
<!-- Output: Calder, United States -->
<!-- Array index access -->
<p>First item: {{items.0.name}}</p>
<!-- Output: First item: Endurance Watch -->
<!-- Numbers resolve as-is -->
<p>Subtotal: {{totals.subtotal}}</p>
<!-- Output: Subtotal: 141 -->
null or undefined and no formatter is
applied, the tag is left as-is (e.g. the literal text {{missing.path}} is preserved). This makes it easy to spot missing data in
your previews.Pipe Syntax
Chain formatters after a data path using the | pipe operator. The resolved
value is passed as the first argument to each formatter, and you can supply additional
literal arguments in parentheses.
<!-- Single formatter -->
{{customer.name | upper}}
<!-- Output: VERONICA COSTELLO -->
<!-- Formatter with argument -->
{{totals.grandTotal | currency('USD')}}
<!-- Output: $162.37 -->
<!-- Chain multiple formatters -->
{{customer.name | trim | upper}}
<!-- Output: VERONICA COSTELLO -->
<!-- Round then format as currency -->
{{totals.grandTotal | round(0) | currency('USD')}}
<!-- Output: $162.00 -->
Helper Call Syntax
Call any helper function directly by name, passing multiple data paths and literals as space-separated arguments. This is ideal when a helper needs two or more data values as input.
<!-- Two data paths as arguments -->
{{add totals.subtotal totals.tax}}
<!-- Output: 151.47 -->
<!-- Data path + literal string -->
{{currency totals.grandTotal "EUR"}}
<!-- Output: €162.37 -->
<!-- Multiple data paths with a literal -->
{{iif totals.discount "Discount Applied" "No Discount"}}
<!-- Output: Discount Applied -->
<!-- Comparison with a literal number -->
{{gt totals.grandTotal 100}}
<!-- Output: true -->
| it uses pipe syntax. If there are 2+ space-separated tokens and the first
token is a known helper name, it uses helper call syntax. Otherwise it is a simple data
path.Argument Types
Each argument after the helper name is resolved based on its format:
| Token | Resolves To | Example |
|---|---|---|
| customer.name | Data path lookup | "Veronica Costello" |
| "USD" | Literal string | "USD" |
| 42 | Literal number | 42 |
| true / false | Literal boolean | true |
| null | Literal null | null |
When to Use Which Syntax
| Use Case | Recommended Syntax | Example |
|---|---|---|
| Format a single value | Pipe | {{totals.grandTotal | currency('USD')}} |
| Chain multiple formatters | Pipe | {{customer.name | trim | upper}} |
| Combine two data values | Helper call | {{add totals.subtotal totals.tax}} |
| Compare data to a literal | Helper call | {{gt totals.grandTotal 100}} |
| Conditional text from data | Helper call | {{iif totals.discount "Yes" "No"}} |
Helper Functions Reference
Shablonix includes 50+ built-in helper functions organized by category. All helpers are
available in both pipe syntax (as formatters) and helper call syntax (as direct function calls). They are also accessible in
custom component code via Lib.Utils.helperName().
Formatting
<!-- Currency: format a number as money -->
{{totals.grandTotal | currency('USD')}} <!-- pipe -->
{{currency totals.grandTotal "USD"}} <!-- helper call -->
<!-- Output: $162.37 -->
{{currency totals.grandTotal "EUR"}} <!-- Output: €162.37 -->
{{currency totals.grandTotal "GBP"}} <!-- Output: £162.37 -->
<!-- Number: locale-aware thousand separators -->
{{totals.subtotal | number}} <!-- Output: 141 -->
<!-- Date: locale-aware date formatting -->
{{invoice.orderDate | date}} <!-- Output: 12/11/2020 -->
<!-- Country: code to display name -->
{{"US" | country}} <!-- Output: United States -->
String
<!-- Case conversion -->
{{customer.name | upper}} <!-- VERONICA COSTELLO -->
{{customer.name | lower}} <!-- veronica costello -->
{{shipping.method | capitalize}} <!-- Flat Rate - Fixed -->
<!-- Trim whitespace -->
{{customer.name | trim}}
<!-- Truncate long text -->
{{customer.name | truncate(8)}} <!-- Veronica... -->
{{truncate customer.name 8}} <!-- Veronica... -->
<!-- Pad strings (great for invoice numbers) -->
{{padStart invoice.number 15 "0"}} <!-- 0000#9000000001 -->
{{padEnd customer.name 20 "."}} <!-- Veronica Costello. -->
<!-- Find & Replace -->
{{replace invoice.number "#" ""}} <!-- 9000000001 -->
{{replace shipping.method " " "-"}} <!-- Flat-Rate---Fixed -->
Math
Math helpers accept multiple data paths, making them especially powerful with helper call syntax.
<!-- Arithmetic with two data paths -->
{{add totals.subtotal totals.tax}} <!-- 151.47 -->
{{sub totals.subtotal totals.discount}} <!-- 126.9 -->
{{mul totals.grandTotal 2}} <!-- 324.74 -->
{{div totals.grandTotal 5}} <!-- 32.474 -->
<!-- Percentage -->
{{percent totals.subtotal 10}} <!-- 14.1 (10% of 141) -->
<!-- Rounding -->
{{round totals.grandTotal 0}} <!-- 162 -->
{{totals.grandTotal | round(1)}} <!-- 162.4 -->
{{floor totals.grandTotal}} <!-- 162 -->
{{ceil totals.grandTotal}} <!-- 163 -->
Compare & Logic
Comparison helpers return true or false. Combine them with iif to produce conditional text directly in any text field.
<!-- Comparisons -->
{{gt totals.grandTotal 100}} <!-- true -->
{{lt totals.grandTotal 100}} <!-- false -->
{{eq payment.method "Check / Money order"}} <!-- true -->
{{ne shipping.method "Express"}} <!-- true -->
{{gte totals.subtotal 141}} <!-- true -->
{{lte totals.subtotal 141}} <!-- true -->
<!-- Logical operators -->
{{not totals.discount}} <!-- false (14.1 is truthy) -->
{{and totals.subtotal totals.tax}} <!-- true (both truthy) -->
{{or totals.discount false}} <!-- true -->
<!-- Conditional text with iif -->
{{iif totals.discount "Discount Applied" "No Discount"}}
<!-- Output: Discount Applied -->
{{iif totals.tax "Taxable" "Tax-Free"}}
<!-- Output: Taxable -->
<!-- Combine comparison + iif with pipe syntax -->
{{totals.grandTotal | gt(200) | iif('Premium Order', 'Standard Order')}}
<!-- Output: Standard Order -->
{{totals.grandTotal | gt(100) | iif('Over $100', 'Under $100')}}
<!-- Output: Over $100 -->
Date
<!-- Add days to a date -->
{{addDays invoice.orderDate 30}} <!-- date + 30 days (ISO) -->
{{addDays "2024-01-15" 30}} <!-- 2024-02-14 -->
<!-- Add months -->
{{addMonths invoice.orderDate 3}} <!-- date + 3 months (ISO) -->
{{addMonths "2024-01-15" 1}} <!-- 2024-02-15 -->
<!-- Days between two dates -->
{{diffDays "2024-01-01" "2024-03-01"}} <!-- 60 -->
{{diffDays invoice.orderDate "2021-01-01"}} <!-- days between dates -->
Array
Array helpers work on list data. In pipe syntax the value is the array; in helper call syntax pass the data path to the array.
<!-- Count items (in component code) -->
Lib.Utils.count(data.items) <!-- 5 -->
<!-- Sum a field from array items -->
Lib.Utils.sum(data.items, 'price') <!-- 141 -->
<!-- Average -->
Lib.Utils.avg(data.items, 'price') <!-- 28.2 -->
<!-- Min / Max -->
Lib.Utils.min(data.items, 'price') <!-- 7 -->
Lib.Utils.max(data.items, 'price') <!-- 50 -->
<!-- Join array to string -->
Lib.Utils.join(['a', 'b', 'c'], ', ') <!-- "a, b, c" -->
<!-- Extract a field from each item -->
Lib.Utils.pluck(data.items, 'name')
<!-- ["Endurance Watch", "Fusion Backpack", ...] -->
<!-- Remove duplicates -->
Lib.Utils.unique([1, 1, 2, 3, 3]) <!-- [1, 2, 3] -->
sum, count, pluck, and join take an array as their first argument. Since the {{...}} interpolation resolves data paths to their values, these helpers
work best in custom component code where you have direct access to the array via Lib.Utils.Name
Extract parts of a person's name. These are particularly useful in greetings, salutations, and personalized content.
<!-- Extract first name -->
{{firstName customer.name}} <!-- Veronica -->
<!-- Extract last name -->
{{lastName customer.name}} <!-- Costello -->
<!-- Pipe syntax works too -->
{{customer.name | firstName}} <!-- Veronica -->
{{customer.name | lastName}} <!-- Costello -->
<!-- Build personalized greetings -->
Dear {{firstName customer.name}} {{lastName customer.name}},
<!-- Output: Dear Veronica Costello, -->
Hello {{firstName customer.name}}!
<!-- Output: Hello Veronica! -->
<!-- If your data has separate first/last fields, just use both: -->
Dear {{customer.firstName}} {{customer.lastName}},
Combining Helpers
The real power comes from combining helpers. Here are practical patterns for common document scenarios.
Invoice Calculations
<!-- Subtotal + Tax, formatted as currency -->
<p>Subtotal: {{currency totals.subtotal "USD"}}</p>
<p>Tax: {{currency totals.tax "USD"}}</p>
<p>Total: {{currency totals.grandTotal "USD"}}</p>
<!-- Compute and display a value -->
<p>Subtotal + Tax = {{add totals.subtotal totals.tax}}</p>
<!-- Output: Subtotal + Tax = 151.47 -->
<!-- Discount percentage -->
<p>Discount: {{percent totals.subtotal 10}}% off</p>
<!-- Output: Discount: 14.1% off -->
Conditional Labels
<!-- Show different text based on data -->
<span>Status: {{iif totals.discount "Discounted" "Full Price"}}</span>
<!-- Threshold-based labels using pipe chaining -->
<span>{{totals.grandTotal | gt(200) | iif('Premium', 'Standard')}}</span>
<span>{{totals.grandTotal | gt(100) | iif('Free Shipping', 'Shipping: $25')}}</span>
<!-- Payment badge -->
<span>{{eq payment.method "Check / Money order"}}</span>
<!-- Output: true -->
Date Calculations
<!-- Payment due date (30 days from order) -->
<p>Due: {{addDays invoice.orderDate 30}}</p>
<!-- Warranty expiration (12 months from order) -->
<p>Warranty expires: {{addMonths invoice.orderDate 12}}</p>
<!-- Days since order -->
<p>{{diffDays invoice.orderDate "2021-06-01"}} days</p>
String Formatting
<!-- Clean up invoice numbers -->
<p>Invoice: {{replace invoice.number "#" "INV-"}}</p>
<!-- Output: Invoice: INV-9000000001 -->
<!-- Uppercase customer name for formal documents -->
<p>{{upper customer.name}}</p>
<!-- Output: VERONICA COSTELLO -->
<!-- Personalized greeting -->
<p>Dear {{firstName customer.name}},</p>
<!-- Output: Dear Veronica, -->
<!-- Truncated description -->
<p>{{truncate customer.name 10}}</p>
<!-- Output: Veronica C... -->
Conditional Content
Show or hide content based on data values.
If/Else Blocks
{{#if is_paid}}
<span class="badge badge-success">Paid</span>
{{else}}
<span class="badge badge-warning">Pending</span>
{{/if}}
<!-- Checking for existence -->
{{#if discount}}
<p>Discount: {{formatCurrency discount}}</p>
{{/if}}
<!-- Negation -->
{{#unless is_draft}}
<p>Document finalized on {{formatDate finalized_at}}</p>
{{/unless}}
Comparison Operators
<!-- Equality -->
{{#if (eq status "completed")}}
<p>Order complete!</p>
{{/if}}
<!-- Not equal -->
{{#if (ne status "cancelled")}}
<p>Order is active</p>
{{/if}}
<!-- Greater/Less than -->
{{#if (gt amount 1000)}}
<p>Large order - free shipping!</p>
{{/if}}
{{#if (lte quantity 5)}}
<p>Low stock warning</p>
{{/if}}
<!-- And/Or logic -->
{{#if (and is_member (gt total 50))}}
<p>Member discount applied</p>
{{/if}}
{{#if (or is_vip has_coupon)}}
<p>Special pricing available</p>
{{/if}}
Loops and Iteration
Iterate over arrays to generate repeated content.
Basic Loop
<table>
<thead>
<tr>
<th>Item</th>
<th>Quantity</th>
<th>Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{this.name}}</td>
<td>{{this.quantity}}</td>
<td>{{formatCurrency this.unit_price}}</td>
<td>{{formatCurrency this.total}}</td>
</tr>
{{/each}}
</tbody>
</table>
Loop Variables
{{#each items}}
<!-- Current index (0-based) -->
<p>{{@index}}. {{this.name}}</p>
<!-- First/Last flags -->
{{#if @first}}
<p class="first-item">First item!</p>
{{/if}}
{{#if @last}}
<p class="last-item">Last item!</p>
{{/if}}
{{/each}}
Empty State
{{#each items}}
<div class="item">{{this.name}}</div>
{{else}}
<p class="empty-state">No items found</p>
{{/each}}
Nested Loops
{{#each categories}}
<h3>{{this.name}}</h3>
<ul>
{{#each this.products}}
<li>{{this.name}} - {{formatCurrency this.price}}</li>
{{/each}}
</ul>
{{/each}}
Best Practices
1. Use Semantic HTML
Structure your templates with proper HTML semantics for better accessibility and consistent rendering.
<!-- Good -->
<article>
<header>
<h1>{{document_title}}</h1>
</header>
<main>
<section>...</section>
</main>
<footer>...</footer>
</article>
<!-- Avoid -->
<div>
<div>
<div>{{document_title}}</div>
</div>
</div>
2. Define a Data Schema
Always define a schema for your templates. This provides validation, better error messages, and documentation for API consumers.
3. Use Print-Friendly CSS
Design with print output in mind. Avoid using viewport units and consider page breaks.
.page-break {
page-break-after: always;
}
.no-break {
page-break-inside: avoid;
}
/* Ensure tables don't split awkwardly */
table {
page-break-inside: avoid;
}
tr {
page-break-inside: avoid;
page-break-after: auto;
}
4. Handle Missing Data Gracefully
Use default values and conditional blocks to handle optional fields.
<!-- Use defaults -->
<p>{{default company_phone "N/A"}}</p>
<!-- Conditional sections -->
{{#if notes}}
<div class="notes-section">
<h4>Notes</h4>
<p>{{notes}}</p>
</div>
{{/if}}
5. Test with Real Data
Always test your templates with realistic data, including edge cases like long text, empty arrays, and special characters.
6. Version Your Templates
Use meaningful names and descriptions. Each update creates a new version, allowing you to track changes over time.