Live View
Build reactive, real-time interfaces without writing JavaScript. Live View renders components on the server and efficiently updates the DOM over WebSockets.
See it in action
Try the interactive counter demo on our landing page.
How It Works
Live View is a server-rendered component system that maintains state on the server. When users interact with the page, events are sent over WebSockets, the server updates the component state, re-renders the template, and sends back only the differences (diffs) to update the DOM.
Creating a Live View Component
Step 1: Create the Template
Create a .sliv file in app/views/live/. Use @variable syntax for reactive state.
<div class="counter-component">
<h2>Count: @count</h2>
<button soli-click="decrement">-</button>
<button soli-click="increment">+</button>
</div>
Step 2: Register the Route
Register your Live View component in config/routes.sl using the router_live function.
# Register LiveView components
router_live("counter", "live#counter");
router_live("metrics", "live#metrics");
Step 3: Create the Controller
Create a controller that handles events. The handler receives an event hash with event (name), params, and state. Return the new state as a hash.
# Counter component handler
fn counter(event_data: Any) -> Any {
event = event_data["event"] # Event name (e.g., "increment")
state = event_data["state"] # Current component state
count = state["count"] || 0
if event == "increment"
{ "count": count + 1 }
elsif event == "decrement"
{ "count": count - 1 }
else
state # Unchanged for unknown events
end
}
Available Directives
soli-click
Triggers on element click. <button soli-click="save">
soli-submit
Triggers on form submission. <form soli-submit="create">
soli-change
Triggers on input value change. <select soli-change="filter">
soli-keydown
Triggers on key press. <input soli-keydown="search">
soli-keyup
Triggers on key release. <input soli-keyup="validate">
soli-focus
Triggers when element gains focus. <input soli-focus="highlight">
soli-blur
Triggers when element loses focus. <input soli-blur="validate">
soli-value-*
Binds input value to state. <input soli-value-name>
soli-target
Specifies target component for updates. <div soli-target="results">
soli-debounce
Debounces event by ms. <input soli-keyup="search" soli-debounce="300">
State Management
State is stored on the server and accessed in templates using @variable syntax. The server maintains state between events.
<!-- Simple variable -->
<span>Hello, @username</span>
<!-- Conditional rendering -->
<% if @logged_in %>
<a href="/logout">Sign Out</a>
<% else %>
<a href="/login">Sign In</a>
<% end %>
<!-- Iteration -->
<% for item in @items %>
<li><%= item["name"] %></li>
<% end %>
Client Setup
Include the Live View client library and mount your component.
<!-- Include the Live View client (~2KB gzipped) -->
<script src="/js/live.js"></script>
<!-- Mount a Live View component -->
<div id="counter" data-live-view="counter"></div>
<script>
// Initialize Live View
SoliLive.connect();
</script>
Route Configuration
Register the Live View WebSocket endpoint in your routes.
# Live View component endpoint
router_live("counter", "live#counter");
Lifecycle Events
Two synthetic events are dispatched by the server in addition to user-driven directives:
connect— fired once, immediately after the WebSocket is established and before any client events. Use it to seed initial state and (optionally) start a tick timer.tick— fired on a recurring interval requested by the handler (see below). Use it for server-pushed updates like dashboards or live charts.
High-Rate Updates with Ticks
For real-time dashboards, monitoring, and live data feeds, a handler can opt into a per-instance recurring tick.
Return the wrapped form { "state": {...}, "tick_interval": <ms> } from any handler invocation —
typically from connect — and the server will fire tick events on that interval.
fn metrics_dashboard(event_data: Any) -> Any {
event = event_data["event"]
if event == "connect"
# Set tick interval in milliseconds (50ms = 20 updates/sec)
{
"state": { "cpu": 0, "memory": 0, "requests": 0 },
"tick_interval": 50
}
elsif event == "tick"
# Server pushes fresh data on each tick
{
"state": {
"time": datetime_now(),
"cpu": system_cpu_usage(),
"memory": system_memory_mb(),
"requests": request_counter
}
}
else
# Unknown event — leave state and tick interval unchanged
event_data["state"]
end
}
<div class="dashboard">
<div class="metric">
<span class="label">Server Time</span>
<span class="value">@time</span>
</div>
<div class="metric">
<span class="label">CPU</span>
<span class="value">@cpu%</span>
</div>
<div class="metric">
<span class="label">Memory</span>
<span class="value">@memory MB</span>
</div>
<div class="metric">
<span class="label">Requests/sec</span>
<span class="value">@requests</span>
</div>
</div>
tick_interval Semantics
The handler may return either shape on any invocation:
- Bare:
{ ...state }— the whole hash is the new state. Equivalent totick_intervalabsent. - Wrapped:
{ "state": {...}, "tick_interval": N }—stateis the new state;tick_intervalcontrols the timer.
| Returned value | Effect |
|---|---|
tick_interval absent | Leave the running tick alone |
0 | Stop the tick |
> 0 | Start (or replace) the tick at this interval, in milliseconds |
If a tick fires while the previous handler call is still running, the tick is dropped (rather than queued) so a slow handler doesn't snowball. Ticks stop automatically when the WebSocket closes.
Recommended Intervals
- 1000ms - Good for dashboards, status pages
- 100ms - Good for live charts, activity feeds
- 50ms (20/s) - Good for real-time monitoring
- 16ms (60/s) - Maximum rate, use sparingly for animations
Performance
Why Live View?
- No JavaScript required - Build interactive UIs with just server-side code
- SEO friendly - Initial HTML is server-rendered
- Reduced complexity - No client-side state management to maintain
- Real-time by default - WebSocket connection enables instant updates