Advanced RxHTML Patterns
This is where RxHTML starts to get interesting — conditional rendering, iterating over collections, composing templates, and the behavior escape hatch for when the declarative approach hits a wall.
Conditional Rendering
rx:if
Show elements when a condition is true:
<div rx:if="is_admin">
<h2>Admin Panel</h2>
<button>Delete Everything</button>
</div>
rx:if can test a few different things:
- Boolean values:
rx:if="is_active"(true when value is true) - Object presence:
rx:if="profile"(true when the object exists) - Value equality:
rx:if="status=published"(true when equal)
rx:ifnot
The inverse of rx:if:
<div rx:ifnot="is_logged_in">
<p>Please log in to continue.</p>
</div>
rx:else
Render alternative content when the condition fails:
<div rx:if="has_items">
<p>Here are your items:</p>
<!-- item list -->
<p rx:else>You have no items yet.</p>
</div>
The rx:else element renders when the parent's condition is false.
force-hiding
By default, rx:if controls whether children exist in the DOM at all. Use force-hiding to toggle visibility via display: none instead:
<div rx:if="is_visible" force-hiding>
This element is always in the DOM, just hidden when false.
</div>
This matters when you need the element to maintain state or position — sometimes ripping things out of the DOM and reinserting them causes problems.
Scoping Behavior
Here's a nice trick: when rx:if tests an object, it also scopes into that object:
<div rx:if="selected_user">
<!-- Now scoped into selected_user -->
<h2><lookup path="name" /></h2>
<p><lookup path="email" /></p>
</div>
If selected_user exists, the div shows up and paths inside are relative to selected_user. Two birds, one attribute.
Iterating Over Collections
rx:iterate
Loop over arrays or lists:
<table>
<tbody rx:iterate="employees">
<tr>
<td><lookup path="name" /></td>
<td><lookup path="email" /></td>
<td><lookup path="department" /></td>
</tr>
</tbody>
</table>
Each iteration scopes into the current element. The path navigates two levels: into the list, then into each item.
Iteration with View State
Expand view state per item:
<div rx:iterate="items" rx:expand-view-state>
<input rx:sync="selected" type="checkbox" />
<lookup path="name" />
</div>
With rx:expand-view-state, each item gets isolated view state. So view:selected is unique per item — you can check individual boxes without them all toggling together.
Options in Select Elements
For <select> elements, use the label attribute since <option> can't contain child elements:
<select rx:iterate="options">
<option value="{id}" label="{name}" />
</select>
rx:repeat
Repeat based on a numeric count:
<div rx:repeat="star_count">
<span>*</span>
</div>
If star_count is 5, this renders five stars. As the number changes, elements get added or removed. Simple.
Switch Statements
rx:switch
Select content based on a string value:
<div rx:switch="card_type">
<p>Your card is:</p>
<div rx:case="face">
A face card: <lookup path="name" />
</div>
<div rx:case="number">
A number card: <lookup path="value" />
</div>
<div rx:case="wild">
A wild card!
</div>
</div>
Only the matching rx:case renders. Content without rx:case always renders (like the <p> above).
Template Composition
Basic Template Usage
<forest>
<template name="card">
<div class="card">
<div class="card-header">
<fragment case="header" />
</div>
<div class="card-body">
<fragment />
</div>
<div class="card-footer">
<fragment case="footer" />
</div>
</div>
</template>
<page uri="/">
<div rx:template="card">
<h2 rx:case="header">Welcome</h2>
<p>Main content goes here.</p>
<button rx:case="footer">Continue</button>
</div>
</page>
</forest>
children-only Attribute
Merge children directly without the wrapper element:
<template name="list-wrapper">
<ul>
<fragment case="items" />
</ul>
</template>
<div rx:template="list-wrapper">
<span rx:case="items" children-only>
<li>Item 1</li>
<li>Item 2</li>
</span>
</div>
The children-only attribute strips the wrapping <span> — only the <li> elements end up in the DOM.
Templates with Iteration
Templates work seamlessly with iteration:
<template name="user-row">
<tr>
<td><lookup path="name" /></td>
<td><lookup path="email" /></td>
<td><fragment /></td>
</tr>
</template>
<table>
<tbody rx:iterate="users">
<tr rx:template="user-row">
<button rx:click="set:selected_user={id}">Select</button>
</tr>
</tbody>
</table>
The Behavior Escape Hatch
When RxHTML's declarative approach can't do what you need (and eventually it won't — I'm realistic about that), use rx:behavior to drop into JavaScript.
Defining Behaviors
In your JavaScript file (linked via the shell):
window.rxhtml.defineBehavior('auto-focus', function(el, connection, config, $) {
el.focus();
});
window.rxhtml.defineBehavior('tooltip', function(el, connection, config, $) {
// Initialize tooltip library
tippy(el, {
content: el.getAttribute('data-tooltip')
});
});
Using Behaviors
<input type="text" rx:behavior="auto-focus" />
<button rx:behavior="tooltip" data-tooltip="Click to save">
Save
</button>
Behavior Parameters
The behavior function receives:
| Parameter | Description |
|---|---|
el |
The DOM element |
connection |
The Adama connection (if within a connection) |
config |
Static configuration from attributes |
$ |
RxHTML framework utilities |
Sending Messages from Behaviors
window.rxhtml.defineBehavior('drag-drop', function(el, connection, config, $) {
el.addEventListener('drop', function(e) {
if (connection) {
connection.send('item_dropped', {
item_id: e.dataTransfer.getData('item_id'),
target_id: el.dataset.targetId
}, {
success: function() { console.log('Sent!'); },
failure: function(reason) { console.error(reason); }
});
}
});
});
rx:custom
For more complex component integration, use rx:custom:
<div rx:custom="chart-component"
parameter:type="line"
parameter:data="sales_data"
port:selected="view:selected_point">
</div>
Parameters prefixed with parameter: become a reactive object. Ports prefixed with port: create functions to write to view state. It's the bridge between RxHTML's reactive world and whatever third-party library you need to use.
Routing and Navigation
Programmatic Navigation
Navigate using the goto command:
<button rx:click="goto:/dashboard">Go to Dashboard</button>
<button rx:click="goto:/product/{id}">View Product</button>
Navigation with Active States
Track the current page in view state:
<template name="nav">
<nav>
<a href="/" class="[view:current=home]active[/]">Home</a>
<a href="/products" class="[view:current=products]active[/]">Products</a>
<a href="/about" class="[view:current=about]active[/]">About</a>
</nav>
<fragment />
</template>
<page uri="/">
<div rx:template="nav" rx:load="set:current='home'">
<h1>Welcome Home</h1>
</div>
</page>
<page uri="/products">
<div rx:template="nav" rx:load="set:current='products'">
<h1>Our Products</h1>
</div>
</page>
The rx:load event sets the current page in view state when the element renders.
Exit Guards
Prevent accidental navigation when there are unsaved changes:
<exit-guard guard="view:has_unsaved_changes" set="view:show_confirm_dialog" />
<div rx:if="view:show_confirm_dialog">
<p>You have unsaved changes. Leave anyway?</p>
<button rx:click="lower:has_unsaved_changes resume">Yes, Leave</button>
<button rx:click="lower:show_confirm_dialog">Stay</button>
</div>
The resume command continues the interrupted navigation. Without it, the user clicks "Yes, Leave" and... nothing happens. Ask me how I know.
Authentication Flows
Sign In Form
<form rx:action="document:sign-in" rx:success="goto:/dashboard">
<input type="text" name="username" placeholder="Username" />
<input type="password" name="password" placeholder="Password" />
<input type="hidden" name="space" value="my-space" />
<input type="hidden" name="key" value="auth-doc" />
<button type="submit">Sign In</button>
</form>
Sign Out
<sign-out />
This element destroys the current identity when rendered. Not a button — an element. It fires on render.
Or wrap it in a page:
<page uri="/logout">
<sign-out />
<p>You have been logged out.</p>
<a href="/login">Sign in again</a>
</page>
Protected Pages
<page uri="/dashboard" authenticate>
<connection use-domain>
<h1>Welcome, <lookup path="username" /></h1>
</connection>
</page>
<page uri="/login" default-redirect-source>
<h1>Please Log In</h1>
<form rx:action="domain:sign-in" rx:success="goto:/dashboard">
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">Log In</button>
</form>
</page>
Server-Side Redirects
RxHTML supports real HTTP redirects (301/302) that happen server-side before any page HTML reaches the browser. There are three mechanisms, from simplest to most dynamic.
Static Rewrites
The <static-rewrite> element defines simple redirect rules directly in your RxHTML. No Adama backend logic required:
<forest>
<!-- 301 permanent redirect (default) -->
<static-rewrite uri="/old-page" location="/new-page" />
<!-- 302 temporary redirect -->
<static-rewrite uri="/promo" location="/products/summer-sale" status="302" />
<!-- Path parameters are captured and substituted into the location using [[name]] -->
<static-rewrite uri="/u/$username:text" location="/profile/[[username]]" />
<!-- Redirect an entire section -->
<static-rewrite uri="/blog/$slug:text" location="/articles/[[slug]]" />
<!-- Wildcard suffix captures the entire remaining path with $name* -->
<static-rewrite uri="/old-docs/$path*" location="/docs/[[path]]" />
<!-- Combine fixed params with a wildcard suffix -->
<static-rewrite uri="/v1/$resource:text/$rest*" location="/v2/[[resource]]/[[rest]]" />
</forest>
| Attribute | Description |
|---|---|
uri |
The path pattern to match (supports $name:type params and $name* wildcard suffix) |
location |
The redirect target — use [[name]] to substitute captured values |
status |
301 (default, permanent) or 302 (temporary) |
The $name* wildcard suffix must be the last segment in the URI. It captures everything remaining in the path as a single string (e.g., /old-docs/foo/bar/baz captures path as foo/bar/baz).
Static rewrites are evaluated at the routing level — they return immediately with no page rendering or backend calls.
Authentication Redirects
Pages with authenticate automatically redirect unauthenticated users. By default, they go to the default-redirect-source page. Override the target per-page with redirect:
<forest>
<!-- Default: unauthenticated users go to /login (the default-redirect-source) -->
<page uri="/dashboard" authenticate>
<connection use-domain>
<h1>Welcome, <lookup path="username" /></h1>
</connection>
</page>
<!-- Override: unauthenticated users on /yolo go to /bounce instead of /login -->
<page uri="/yolo" authenticate redirect="/bounce">
<p>Special page</p>
</page>
<page uri="/bounce">
<p>You were redirected here.</p>
</page>
<page uri="/login" default-redirect-source>
<h1>Please Log In</h1>
</page>
</forest>
When SSR detects no authenticated principal, it returns a 302 to the redirect target. The redirect attribute accepts path expressions that can include view-state variables (e.g., redirect="/login/$view:return_to").
Inline Remote Redirects
Pages using <remote-inline> can trigger server-side redirects from the Adama @web handler. When the handler returns a forward response, the SSR pipeline converts it into a 302:
<forest>
<page uri="/go/$code:text">
<remote-inline path="/resolve" />
</page>
</forest>
@web get /resolve {
let code = @parameters.str("code", "");
if (code == "home") {
return { forward: "/dashboard" };
}
return { html: "<p>Unknown code</p>" };
}
The forward field produces a 302 (temporary redirect) and redirect produces a 301 (permanent redirect). During SSR, either one results in a redirect sent to the browser before any page HTML is rendered.
Error Handling
Form Errors
<form rx:action="send:create_item"
rx:success="lower:show_form raise:show_success"
rx:failure="raise:show_error">
<input type="text" name="title" />
<button type="submit">Create</button>
<div rx:if="view:show_error" class="error">
Failed to create item. Please try again.
</div>
</form>
<div rx:if="view:show_success" class="success">
Item created successfully!
</div>
Connection Errors
Handle connection failures with a redirect:
<connection space="my-space" key="my-key" redirect="/error">
<!-- Normal content -->
</connection>
Or show an inline error:
<connection space="my-space" key="my-key">
<div>Connected content</div>
<div rx:else>
<p>Unable to connect. Check your connection and try again.</p>
<button rx:click="goto:/">Return Home</button>
</div>
</connection>
Monitoring Values
The Monitor Element
Watch for value changes:
<monitor path="notification_count"
delay="100"
rx:rise="raise:show_notification"
rx:fall="lower:show_notification" />
| Attribute | Purpose |
|---|---|
path |
Value to watch |
delay |
Debounce delay in milliseconds |
rx:rise |
Command when value increases |
rx:fall |
Command when value decreases |
rx:monitor Attribute
Monitor on any element:
<div rx:monitor="unread_count" rx:rise="raise:flash_badge">
<span class="badge"><lookup path="unread_count" /></span>
</div>
At this point you've got the tools to build pretty much anything. The declarative approach covers 90% of cases; behaviors cover the rest. Between conditionals, iteration, templates, and the event command language, most reactive UIs fall into place without writing JavaScript at all.