Data Binding
RxHTML connects to Adama documents via WebSocket and keeps the UI in sync with server data automatically. This chapter covers establishing connections, displaying data, and sending messages — the bread and butter of building anything with RxHTML.
The Connection Element
The <connection> element establishes a WebSocket connection to an Adama document:
<forest>
<page uri="/">
<connection space="my-space" key="my-key">
<!-- Everything inside has access to document data -->
<h1><lookup path="title" /></h1>
</connection>
</page>
</forest>
Connection Modes
Connect by space and key:
<connection space="my-space" key="my-key">
<!-- Direct document reference -->
</connection>
Or connect using domain mapping (handy for multi-tenant setups):
<connection use-domain>
<!-- Document determined by the domain -->
</connection>
Connection Attributes
| Attribute | Purpose |
|---|---|
space |
The Adama space name |
key |
The document key within the space |
use-domain |
Use domain mapping instead of space/key |
name |
Name this connection for reference elsewhere |
identity |
Override the identity for authorization |
redirect |
Redirect to this page on connection failure |
keep-open |
Keep rendering even if connection drops |
Connection Status
Children of <connection> only render when connected. You can handle the disconnected state too:
<connection space="my-space" key="my-key">
<div>Connected! Here is your data...</div>
<div rx:else>Connecting...</div>
</connection>
Named Connections
Name a connection so you can reference it from elsewhere on the page:
<connection space="my-space" key="my-key" name="main">
<!-- Main content -->
</connection>
<!-- Elsewhere in the page -->
<pick name="main">
<!-- Uses the same connection -->
<lookup path="some_field" />
</pick>
The <pick> element reuses an existing named connection. This avoids opening a second WebSocket just to read from the same document in a different part of the page.
Connection Status Element
Display connection status without messing with rendering of children:
<connection-status name="main">
<span>Connected</span>
<span rx:disconnected>Reconnecting...</span>
</connection-status>
Displaying Data
The Lookup Element
Display a value from the document as text:
<connection space="my-space" key="my-key">
<h1><lookup path="title" /></h1>
<p>By <lookup path="author" /></p>
<span><lookup path="views" /> views</span>
</connection>
Lookup Transforms
Transform values before display:
<lookup path="created_at" transform="time_ago" />
<lookup path="name" transform="upper" />
<lookup path="file_size" transform="size_bytes" />
Available transforms:
| Transform | Description |
|---|---|
trim |
Trim whitespace |
upper |
Convert to uppercase |
lower |
Convert to lowercase |
time_ago |
Convert datetime to "3 hours ago" format |
time |
Format time for display |
size_bytes |
Format bytes as KB, MB, GB |
jsonify |
Convert to JSON string |
principal.agent |
Extract agent from principal |
principal.authority |
Extract authority from principal |
vulgar_fraction |
Convert decimal to unicode fraction |
Lookup Refresh
For time-based transforms, you probably want periodic refreshes:
<lookup path="created_at" transform="time_ago" refresh="60000" />
The refresh attribute is in milliseconds. So 60000 = update the "3 hours ago" text every minute.
Attribute Binding
Bind document values to HTML attributes using curly braces:
<img src="{profile_image_url}" alt="Profile for {username}" />
<a href="/user/{user_id}">View Profile</a>
Conditional Attribute Values
Attributes support conditional expressions, which is where things get interesting:
<!-- Boolean toggle -->
<div class="[is_active]active[/]">
Shows "active" class when is_active is true
</div>
<!-- Boolean with else branch -->
<div class="[is_active]active[#]inactive[/]">
Shows "active" or "inactive" based on is_active
</div>
<!-- Value comparison -->
<div class="[status=published]public[#]draft[/]">
Shows "public" if status equals "published", else "draft"
</div>
<!-- Combine with literal text -->
<div class="card [highlighted]card-highlighted[/]">
Always has "card" class, conditionally has "card-highlighted"
</div>
Trusted HTML
Render HTML content directly (be careful with this one):
<trusted-html path="html_content" />
This sets innerHTML directly. Only use it with content you control; otherwise you're asking for trouble.
Dynamic Titles
Set the page title reactively:
<title value="Messages for {username}" />
Scoping into Data
Scope into Objects
Navigate into nested objects so you don't have to write long paths everywhere:
<div rx:scope="user">
<lookup path="name" />
<lookup path="email" />
</div>
Equivalent to:
<lookup path="user/name" />
<lookup path="user/email" />
Scope and View State Expansion
By default, scoping only affects data paths. Use rx:expand-view-state to also scope the view state:
<div rx:scope="settings" rx:expand-view-state>
<!-- view:field here refers to view:settings/field -->
</div>
This matters when each item needs its own isolated view state — think per-row UI toggles in a table.
Forms and Sending Messages
Basic Form Submission
Send messages to Adama channels using forms:
<form rx:action="send:create_task">
<input type="text" name="title" placeholder="Task title" />
<button type="submit">Create</button>
</form>
The form fields get collected into a message matching the channel's expected type. This form sends to the create_task channel with something like { title: "..." }. No serialization code, no fetch calls — just a form.
Form Actions
| Action | Purpose |
|---|---|
send:channel_name |
Send message to channel |
domain:sign-in |
Authenticate via domain document |
domain:sign-in-reset |
Authenticate and change password |
document:sign-in |
Authenticate to specific document |
document:put |
HTTP PUT to document web endpoint |
copy:path |
Copy form values to view state |
custom:verb |
Run custom JavaScript handler |
Sending with Checkboxes
For toggle operations, combine a form with click events:
<form rx:action="send:toggle_task">
<input type="hidden" name="task_id" value="{id}" />
<input type="checkbox" rx:checked="done" />
<button type="submit">Toggle</button>
</form>
Success and Failure Handling
Handle form submission results:
<form rx:action="send:create_task"
rx:success="goto:/tasks"
rx:failure="raise:show_error">
<input type="text" name="title" />
<button type="submit">Create</button>
<div rx:if="view:show_error">
Something went wrong. Please try again.
</div>
</form>
Password Handling
RxHTML treats password fields specially for security:
- Fields named
passwordare never sent to the server in plain text - For channel messages, passwords are hashed before sending
- For authentication, passwords flow through the auth mechanism
Special password field names:
| Field Name | Purpose |
|---|---|
password |
Primary password (hashed before sending) |
new_password |
New password for password changes |
confirm-password |
UI validation only, stripped before send |
confirm-new_password |
UI validation only, stripped before send |
View State Synchronization
Sync Input to View State
Synchronize input values to the view state:
<input type="text" rx:sync="search_filter" />
As the user types, view:search_filter updates. Your Adama code can use this to filter results in real time:
record Item {
public string title;
}
table<Item> _items;
view string search_filter;
bubble filtered_results = iterate _items
where @viewer.search_filter == "" || title.contains(@viewer.search_filter);
This is the feedback loop in action — user types, view state updates, Adama recomputes the bubble, delta flows back, UI refreshes. All without a single line of JavaScript.
Debounce Updates
Control how often sync updates get sent:
<input type="text" rx:sync="search" rx:debounce="300" />
Updates fire at most every 300 milliseconds. Without this, you'd hammer the server on every keystroke.
URL Parameter Sync
Synchronize view state with URL parameters:
<view-state-params sync:page="page" sync:filter="filter" />
This keeps view:page and view:filter in sync with ?page=...&filter=... in the URL. Good for shareable/bookmarkable state.
Event Handling
RxHTML has a command language for handling events:
<button rx:click="toggle:show_details">Toggle Details</button>
<button rx:click="set:tab='settings'">Settings Tab</button>
<button rx:click="inc:counter">+1</button>
Available Commands
| Command | Effect |
|---|---|
toggle:path |
Toggle boolean in view state |
set:path=value |
Set value in view state |
raise:path |
Set boolean to true |
lower:path |
Set boolean to false |
inc:path |
Increment number |
dec:path |
Decrement number |
goto:uri |
Navigate to URI |
fire:channel |
Send empty message to channel |
submit |
Submit the containing form |
reset |
Reset the containing form |
Multiple Commands
Chain commands with spaces:
<button rx:click="set:tab='main' lower:show_modal">
Close and Go to Main
</button>
String Values
Use single quotes for string values with spaces:
<button rx:click="set:title='Hello World'">Set Title</button>
Event Types
| Attribute | Event |
|---|---|
rx:click |
Element clicked |
rx:mouseenter |
Mouse entered element |
rx:mouseleave |
Mouse left element |
rx:change |
Input value changed |
rx:blur |
Input lost focus |
rx:focus |
Input gained focus |
rx:check |
Checkbox checked |
rx:uncheck |
Checkbox unchecked |
rx:load |
Element was rendered |
rx:success |
Form submission succeeded |
rx:failure |
Form submission failed |
Now go learn about advanced patterns — conditionals, iteration, and the escape hatches for when the declarative approach isn't enough.