Placeholders
To put a value on the page, you write {|path|} right there in the
HTML — no script, no wiring. When the page renders, the placeholder is replaced with the data.
A placeholder names a path into your data. For a top-level value that's just a name; for something nested you use a dotted path that walks down into the object:
{|name|}— a single, top-level value.{|order.status|}— a dotted path: thestatusinsideorder.{|order.customer.city|}— keep going as deep as the data does.
The delimiters are {| and |}. That's the whole syntax
for a value. They are not {{ }} (that's a different templating world) and not
{# #} — if you reach for those out of habit, nothing will resolve.
<!-- a name, a dotted status, and a lookup field -->
<h2>Order for {|customer__m.name|}</h2>
<p>Status: <strong>{|order.status|}</strong></p>
<p>Placed by {|order.owner.name|} on {|order.created|}</p>Where does the data come from? Every placeholder resolves against the current
record plus the page's pageData — and when both supply the same
key, pageData wins. So a page_onload function can shape or override
exactly what the template shows, without you touching the HTML.
.name
A lookup field doesn't hold a plain string; it holds an object shaped like
{_id, name}. Display it with its .name:
{|customer__m.name|} is correct. Writing the bare
{|customer__m|} drops the whole object onto the page and renders the useless
[object Object]. When in doubt, reach for .name.
Loops with #each
To render a list, you wrap the markup that should repeat between
{|#each|} and {|/each|}. Everything inside the block is drawn once per
item in the array.
Inside the block, the current item is addressed with a leading dot:
{|.field|} means "the field of the item we're on right now." Two helpers
ride along for free:
{|.$index|}— the 0-based position (0, 1, 2, …).{|.$position|}— the 1-based position (1, 2, 3, …) — friendlier for "row 1, row 2".
Here the table's <tr> sits between the #each tags, so one
row is produced for every order in the orders array:
<table>
<thead>
<tr><th>#</th><th>Order</th><th>Customer</th><th>Status</th></tr>
</thead>
<tbody>
{|#each orders|}
<tr>
<td>{|.$position|}</td>
<td>{|.name|}</td>
<td>{|.customer__m.name|}</td>
<td>{|.status|}</td>
</tr>
{|/each|}
</tbody>
</table>Notice the customer cell uses {|.customer__m.name|} — the same
.name rule from Unit 1, just on the current item.
#each must wrap the repeating HTML
The repeated markup has to live inside the block. If you put the <tr>
outside the {|#each|}…{|/each|}, it isn't part of the loop, so it
renders once with nothing to bind to. And {|.field|} only resolves inside
an each block — use it anywhere else and it has no current item to read from. An empty each
(an array with zero items) simply renders nothing — which is exactly the right behaviour for an
empty list.
Conditionals with #if
To show, hide, or swap markup based on a value, wrap it in
{|#if cond|}…{|/if|} — with an optional {|#else|} for the
other branch.
A condition compares two things with one of six operators:
==equal ·!=not equal>greater ·<less>=greater-or-equal ·<=less-or-equal
String values in a comparison go in single quotes, e.g.
{|#if order.status == 'Shipped'|}. And because #if is just markup, you can
drop one inside an #each to give every row its own badge:
<!-- a swap: one branch or the other -->
{|#if order.status == 'Shipped'|}
<span class="badge badge--ok">On its way</span>
{|#else|}
<span class="badge badge--wait">Preparing</span>
{|/if|}
<!-- an #if inside an #each row -->
{|#each orders|}
<tr>
<td>{|.name|}</td>
<td>
{|#if .total > 1000|}<span class="tag">High value</span>{|/if|}
{|.status|}
</td>
</tr>
{|/each|}A template #if is for a single, readable comparison — show this, else show that.
When the decision gets complicated (multiple ANDs, lookups, computed flags), don't cram it into the
markup. Shape the data in the page_onload function instead — compute a tidy
statusLabel or a boolean isOverdue there, then the template just reads it.
Complex logic belongs in the function; the template only displays.
Display belongs in HTML
Here's the golden rule of rtext, and the one most worth remembering: never build display with JavaScript.
That means no innerHTML, no textContent assignments, no hand-written
for loops stitching strings together, and no "if this then show that" branching done in
script. All of that is the template's job. Placeholders resolve automatically against
record + pageData, and they re-render on their own whenever you
push new data with ctx.set. Even a count like {|orders.length|} works —
.length is just another dotted path the resolver walks.
So the division of labour is clean: JavaScript shapes the data, the HTML displays it.
You call ctx.set('orders', list) and the {|#each orders|} block redraws
itself. Compare the two approaches:
// WRONG: assembling HTML by hand in script
var html = '';
for (var i = 0; i < orders.length; i++) {
html += '<tr><td>' + orders[i].name +
'</td><td>' + orders[i].status + '</td></tr>';
}
document.querySelector('#rows').innerHTML = html;<!-- RIGHT: the HTML owns the display -->
<tbody id="rows">
{|#each orders|}
<tr><td>{|.name|}</td><td>{|.status|}</td></tr>
{|/each|}
</tbody>
// ...and in script, you only ever hand it data:
ctx.set('orders', orders); // the each block re-renders itselfThe "right" version is shorter, safe from broken-string bugs, and impossible to get out of sync — because there's only one source of truth for the markup. Once you internalise this, rtext pages stop fighting you.
You can now display anything. Module R3 — Binding Fields & Buttons — turns it two-way: capturing what the user types and wiring buttons back to your functions. New term in this module? It's in the Glossary.
Earn the Templater stamp
Answer all four to lock in Module R2.