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.
The template declares the view. Your context supplies its state and behavior.
Try it live
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(...):
- HTML string (
template) - Existing DOM element (
element) - DOM selector (
selector) - JSON template (
json)
String templates are preprocessed before DOM parsing (table compatibility rules below).
Interpolation
Regor supports two interpolation delimiters:
{{ expr }}[[ expr ]]
<p>{{ user.name }}</p>
<p>[[ user.name ]]</p> Interpolation Behavior
When interpolation is enabled (RegorConfig.useInterpolation = true):
- Interpolation expressions in text nodes become reactive text.
- Interpolation works as the whole text content or mixed with surrounding text.
- Subtrees marked with
r-preare 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:
- 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) withis="r-*"markers.
- In table-sensitive positions:
- non-native direct children may be rewritten to safe hosts with
is="regor:OriginalTag".
- non-native direct children may be rewritten to safe hosts with
- 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:
- Text/content:
r-text, interpolation - Attributes/properties:
:x,.x,r-bind:x - Events:
@click,r-on:* - Control flow:
r-if,r-else-if,r-for - Styling:
:class,:style - Components:
:is,:context,r-context
Expression Scope and Resolution
Expressions are evaluated against active context stack:
- Current app/component context
- Loop aliases (
r-for) - Slot switch contexts (when
head.enableSwitch = true) - Global context from
RegorConfig
Special identifiers:
$root: root-most context$parent: immediate parent context$ctx: full context stack arraythis: current context object$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:
- Member access, optional chaining:
a.b,a?.b,a[k] - Calls/new:
fn(x),new Date(0) - Arithmetic/comparison/logical/bitwise operators
- Nullish coalescing / ternary:
a ?? b,ok ? a : b - Assignments and updates:
x = 1,x += 2,x++,--x - Arrays/objects/spread/computed object keys
- Arrow functions
- Template literals and tagged templates
- Regex literals
- Comma/compound forms
r-for Key Notes
Both forms are supported for expression-based keys:
key="row.id":key="row.id"
Nested paths such as a.b.c.d are supported. Use stable domain IDs for predictable updates.
Use an item identity such as service.id. A position in a filtered list does not identify the same item over time.
Practical Patterns
- Prefer
r-textfor hot repeated rows. - Keep template expressions lightweight; move heavy logic into methods/computed refs.
- Use stable keys in lists.
- Use
r-preto 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:
:key="row.id"for stable identity.- Keep row expressions simple (
r-text+ direct paths). - 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.