ESC
Type to search...
S
Soli Docs

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.

Try Live Demo

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.

User Event
Click, Submit, Change
Server
Update State & Render
DOM Update
Apply Diff Patch

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 to tick_interval absent.
  • Wrapped: { "state": {...}, "tick_interval": N }state is the new state; tick_interval controls the timer.
Returned value Effect
tick_interval absentLeave the running tick alone
0Stop the tick
> 0Start (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

~2KB
Client Size (gzipped)
<50ms
Round-trip Latency
Diff
Minimal Updates

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