Templates and Expressions

Connect HTML to state with expressions, keyed lists, events, and form bindings.

This page describes how Regor turns templates into live bindings, and what expression syntax is supported in runtime.

ExpressionsListsForms

The template declares the view. Your context supplies its state and behavior.

Try it live

A service board that respondsLive Regor

4 of 4 services · illustrative data

  • Public APIFrankfurt
  • Search indexAmsterdam
  • Background jobsFrankfurt
  • Media deliveryLondon

Filter the list, then click a status to change it. Stable keys identify each row.

import { computed, html, ref, sref } from 'regor'

const initialServices = [
  { id: 'api', name: 'Public API', region: 'Frankfurt', healthy: true },
  { id: 'search', name: 'Search index', region: 'Amsterdam', healthy: false },
  { id: 'jobs', name: 'Background jobs', region: 'Frankfurt', healthy: true },
  { id: 'media', name: 'Media delivery', region: 'London', healthy: true },
]

export function createServices(interactive = false) {
  const query = ref('')
  const status = ref('all')
  const services = sref(initialServices.map((service) => ({ ...service })))
  const visible = computed(() =>
    services().filter((service) => {
      const matchesText = `${service.name} ${service.region}`
        .toLowerCase()
        .includes(query().trim().toLowerCase())
      return (
        matchesText &&
        (status() === 'all' || service.healthy === (status() === 'healthy'))
      )
    }),
  )
  // The row objects are plain values: replace the shallow ref's array to notify.
  const toggle = (id: string) =>
    services(
      services().map((service) =>
        service.id === id ? { ...service, healthy: !service.healthy } : service,
      ),
    )
  const reset = () => {
    query('')
    status('all')
  }
  return { interactive, query, status, services, visible, toggle, reset }
}

export const servicesTemplate = html` <div class="guide-demo">
  <div class="guide-filter">
    <label
      >Find a service<input
        type="search"
        placeholder="Try Frankfurt..."
        r-model="query"
        :disabled="!interactive"
    /></label>
    <label
      >Status<select r-model="status" :disabled="!interactive">
        <option value="all">All services</option>
        <option value="healthy">Healthy</option>
        <option value="degraded">Degraded</option>
      </select></label
    >
  </div>
  <p class="guide-result-count" role="status">
    {{ visible.length }} of {{ services.length }} services · illustrative data
  </p>
  <ul class="guide-service-list">
    <li r-for="service in visible" :key="service.id">
      <div>
        <strong>{{ service.name }}</strong><small>{{ service.region }}</small>
      </div>
      <button
        type="button"
        :class="{ 'is-healthy': service.healthy }"
        :aria-label="'Toggle status of ' + service.name"
        @click="toggle(service.id)"
        :disabled="!interactive"
      >
        <i aria-hidden="true"></i>{{ service.healthy ? 'Healthy' : 'Degraded' }}
      </button>
    </li>
  </ul>
  <div r-if="visible.length === 0" class="guide-empty">
    <strong>No matching services.</strong>
    <p>Try a different search or status.</p>
    <button type="button" @click="reset" :disabled="!interactive">
      Clear filters
    </button>
  </div>
  <p class="guide-hint">
    Filter the list, then click a status to change it. Stable keys identify each
    row.
  </p>
</div>`
import { createApp, useScope } from 'regor'
import { createQuote, quoteTemplate } from './quote'
import { createServices, servicesTemplate } from './services'
import { createProfile, profileTemplate } from './profile'
import { createLifecycle, lifecycleTemplate } from './lifecycle'

// Each guide mounts only its own island. Everything outside the root stays static.
function mount<T extends object>(
  id: string,
  create: (interactive: boolean) => T,
  template: string,
) {
  const element = document.getElementById(id)
  if (element)
    createApp(
      useScope(() => create(true)),
      { element, template },
    )
}

mount('guide-quote', createQuote, quoteTemplate)
mount('guide-services', createServices, servicesTemplate)
mount('guide-profile', createProfile, profileTemplate)
mount('guide-lifecycle', createLifecycle, lifecycleTemplate)

Search by name or region, filter by status, and change individual services. The list uses r-model, r-if, and a keyed r-for.

Template Sources

Regor accepts templates from multiple sources in createApp(...) / defineComponent(...):

  1. HTML string (template)
  2. Existing DOM element (element)
  3. DOM selector (selector)
  4. JSON template (json)

String templates are preprocessed before DOM parsing (table compatibility rules below).

Interpolation

Regor supports two interpolation delimiters:

  1. {{ expr }}
  2. [[ expr ]]
<p>{{ user.name }}</p>
<p>[[ user.name ]]</p>

Interpolation Behavior

When interpolation is enabled (RegorConfig.useInterpolation = true):

  1. Interpolation expressions in text nodes become reactive text.
  2. Interpolation works as the whole text content or mixed with surrounding text.
  3. Subtrees marked with r-pre are skipped.

If useInterpolation = false, {{ ... }} and [[ ... ]] stay as plain text.

Table-Safe Preprocess for String Templates

For string templates, Regor preprocesses markup to keep table structures valid:

  1. Outside table scope:
    • <caption> / <thead> / <tbody> / <tfoot> / <tr> / <td> / <th> / <colgroup> / <col> may be rewritten to alias hosts (captionx / theadx / tbodyx / tfootx / trx / tdx / thx / colgroupx / colx) with is="r-*" markers.
  2. In table-sensitive positions:
    • non-native direct children may be rewritten to safe hosts with is="regor:OriginalTag".
  3. Self-closing custom tags under row context are normalized to explicit open/close tags.

This avoids browser table parser dropping/reparenting custom nodes.

Where Expressions Are Used

Typical expression points:

  1. Text/content: r-text, interpolation
  2. Attributes/properties: :x, .x, r-bind:x
  3. Events: @click, r-on:*
  4. Control flow: r-if, r-else-if, r-for
  5. Styling: :class, :style
  6. Components: :is, :context, r-context

Expression Scope and Resolution

Expressions are evaluated against active context stack:

  1. Current app/component context
  2. Loop aliases (r-for)
  3. Slot switch contexts (when head.enableSwitch = true)
  4. Global context from RegorConfig

Special identifiers:

  1. $root: root-most context
  2. $parent: immediate parent context
  3. $ctx: full context stack array
  4. this: current context object
  5. $event: available in event/lazy evaluation paths

Regor auto-unrefs refs in expressions, so template code uses user.name, not user.name() / .value.

Supported Expression Syntax (Runtime)

Backed by Regor’s jsep/regorEval pipeline, templates support:

  1. Member access, optional chaining: a.b, a?.b, a[k]
  2. Calls/new: fn(x), new Date(0)
  3. Arithmetic/comparison/logical/bitwise operators
  4. Nullish coalescing / ternary: a ?? b, ok ? a : b
  5. Assignments and updates: x = 1, x += 2, x++, --x
  6. Arrays/objects/spread/computed object keys
  7. Arrow functions
  8. Template literals and tagged templates
  9. Regex literals
  10. Comma/compound forms

r-for Key Notes

Both forms are supported for expression-based keys:

  1. key="row.id"
  2. :key="row.id"

Nested paths such as a.b.c.d are supported. Use stable domain IDs for predictable updates.

Give changing lists stable keys

Use an item identity such as service.id. A position in a filtered list does not identify the same item over time.

Practical Patterns

  1. Prefer r-text for hot repeated rows.
  2. Keep template expressions lightweight; move heavy logic into methods/computed refs.
  3. Use stable keys in lists.
  4. Use r-pre to protect literal template snippets from interpolation transform.

Advanced Samples

1) Static-first page + dynamic island

Use existing server-rendered markup and bind only the dynamic area.

<header>
  <h1>My Shop</h1>
  <div id="auth-island">
    <button @click="toggle">{{ loggedIn ? 'Logout' : 'Login' }}</button>
    <span r-show="loggedIn">Welcome, {{ userName }}</span>
  </div>
</header>
createApp(
  {
    loggedIn: ref(false),
    userName: ref('Ada'),
    toggle() {
      this.loggedIn(!this.loggedIn())
    },
  },
  { selector: '#auth-island' },
)

2) Large list with stable keys and cheap row expressions

<ul>
  <li r-for="row, #i in rows" :key="row.id">
    <strong r-text="row.title"></strong>
    <span>#{{ i }}</span>
    <small r-text="row.status"></small>
  </li>
</ul>

Pattern:

  1. :key="row.id" for stable identity.
  2. Keep row expressions simple (r-text + direct paths).
  3. Avoid heavy object construction inside row template expressions.

3) Dynamic component template switching (:is)

<section :is="currentView" :context="{ item: selectedItem }"></section>
createApp({
  currentView: ref('UserView'),
  selectedItem: ref({ id: 1, name: 'Ada' }),
  components: { UserView, AuditView },
})

Use this when one region must swap templates/components based on state, while keeping parent bindings simple.

4) Slot template evaluated in parent context

const Shell = defineComponent('<section><slot></slot></section>', {
  context: (head) => {
    head.enableSwitch = true
    return {}
  },
})
<Shell>
  <p>{{ parentMessage }}</p>
</Shell>

With head.enableSwitch = true, slot expressions can resolve against parent context naturally.

5) Interpolation-off region for literal template text

const cfg = new RegorConfig()
cfg.useInterpolation = false
createApp(
  { msg: ref('x') },
  { element: root, template: '<pre>{{ msg }}</pre>' },
  cfg,
)

or local skip:

<pre r-pre>
  {{ this stays literal }}
</pre>

Use these when docs/snippets must display {{ ... }} without binding.

See Also

  1. Directives
  2. Reactivity
  3. Components
  4. r-for