Aquargin Way / Master rtext / Module R2

Templates & Display

An rtext page shows data the moment you write a placeholder in the HTML. In this module you'll drop in single values, repeat rows with #each, branch with #if — and learn the one rule that keeps rtext pages sane: display lives in the markup, never in script.

4 units ~14 min Earn the Templater stamp
Unit 1 of 4

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: the status inside order.
  • {|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.

order-summary.html
<!-- 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.

Lookup fields are objects — use .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.

Unit 2 of 4

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:

orders-table.html
<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.

The #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.

Unit 3 of 4

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:

status-badges.html
<!-- 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|}
Keep conditions simple

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.

Unit 4 of 4

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 — building display in JS
// 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 — #each in HTML, ctx.set the data
<!-- 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 itself

The "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.

Next: bind the inputs

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.

Checkpoint

Earn the Templater stamp

Answer all four to lock in Module R2.