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>

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.

Previous Data Binding