Real-Time Views
This guide explains how to implement real-time interfaces with Live::View
.
What is Live::View
?
Live::View
enables real-time, interactive web interfaces using WebSocket connections. It allows you to update the DOM in real-time without JavaScript, making it perfect for dynamic content that changes frequently.
When to use Live::View
:
- Real-time dashboards and status displays.
- Interactive forms with live validation.
- Live updating content (clocks, counters, progress bars).
- Simple games and interactive applications.
When NOT to use Live::View
:
- Static content that doesn't change.
- Complex client-side interactions requiring heavy JavaScript.
Setup
Import Maps Configuration
Use bin/importmap
to install the required JavaScript packages:
> bin/importmap pin @socketry/live
Pinning "@socketry/live" to vendor/javascript/@socketry/live.js via download from https://ga.jspm.io/npm:@socketry/live@0.14.0/Live.js
Pinning "morphdom" to vendor/javascript/morphdom.js via download from https://ga.jspm.io/npm:morphdom@2.7.7/dist/morphdom-esm.js
JavaScript Setup
Create app/javascript/live.js
to initialize Live::View
:
import {Live} from "@socketry/live"
window.live = Live.start()
Pin your local live.js
file in config/importmap.rb
:
pin "live"
Key Points:
- Import maps handle the
@socketry/live
package automatically. - The
live.js
file starts theLive::View
client connection. pin "live"
makes your locallive.js
file importable in view templates.window.live
makes the Live connection globally available.
Basic Implementation: Live Clock
Live::View Class
Create a Live::View
class that handles the real-time logic:
require 'live'
class ClockTag < Live::View
def initialize(...)
super
end
def bind(page)
@task ||= start_clock
end
def close
if task = @task
@task = nil
task.stop
end
end
def start_clock
Async do
while true
sleep 1
self.update!
end
end
end
def forward_event(name)
"event.preventDefault(); live.forwardEvent(#{JSON.dump(@id)}, event, {name: #{name.inspect}})"
end
def render(builder)
builder.tag(:div, class: "clock-container") do
builder.tag(:h2) {builder.text("Live Clock")}
builder.tag(:div, id: "clock", class: "clock-display") do
builder.text(Time.now.strftime("%H:%M:%S"))
end
end
end
end
Key Points:
- Inherit from
Live::View
to get real-time capabilities. - Use
Async
blocks for background tasks. - Use
self.update!
to queue a full re-render. - Define
forward_event
method to handle user interactions. - The
render
method defines the initial HTML structure.
Controller
Create a controller to handle the Live::View connection:
require 'async/websocket/adapters/rails'
class ClockController < ApplicationController
RESOLVER = Live::Resolver.allow(ClockTag)
def index
@tag = ClockTag.new('clock')
end
skip_before_action :verify_authenticity_token, only: :live
def live
self.response = Async::WebSocket::Adapters::Rails.open(request) do |connection|
Live::Page.new(RESOLVER).run(connection)
end
end
end
Key Points:
- Use
Live::Resolver.allow
to whitelist yourLive::View
sub-classes. - Skip CSRF verification for the WebSocket endpoint.
- Use
Async::WebSocket::Adapters::Rails.open
for WebSocket handling. Live::Page.new(RESOLVER).run(connection)
handles theLive::View
protocol.
View Template
Create a view template that renders the Live::View:
<h1>⏰ Live Clock Example</h1>
<p>This clock updates every second using Live::View.</p>
<%= javascript_import_module_tag "live" %>
<div class="clock-wrapper">
<%= raw @tag.to_html %>
</div>
<style>
.clock-container {
text-align: center;
padding: 2rem;
border: 2px solid #ddd;
border-radius: 8px;
margin: 2rem 0;
}
.clock-display {
font-size: 3rem;
font-family: 'Monaco', 'Consolas', monospace;
color: #333;
background: #f0f0f0;
padding: 1rem;
border-radius: 4px;
margin-top: 1rem;
}
</style>
Key Points:
- Use
<%= javascript_import_module_tag "live" %>
to load Live::View on specific pages. - Use
raw @tag.to_html
to render the Live::View component. - The Live::View will automatically connect via WebSocket.
- This loads Live::View only on pages that need it, not globally.
Routing Configuration
Add routes to your config/routes.rb
:
Rails.application.routes.draw do
# Live Clock Example:
get "clock/index"
connect "clock/live"
end
Key Points:
- Use the Rails 8
connect
helper for WebSocket routes. - The
live
action handles the WebSocket connection.
Live::View Class with User Interaction
class CounterTag < Live::View
def initialize(count: 0)
super
# @data is persisted on the tag in `data-` attributes.
@data["count"] = @data.fetch("count", count).to_i
end
def handle(event)
case event[:type]
when "click"
case event.dig(:detail, :name)
when "increment"
@data["count"] += 1
when "decrement"
@data["count"] -= 1
end
update_counter
end
end
def update_counter
self.replace("#counter") do |builder|
builder.tag(:div, id: "counter", class: "counter-display") do
builder.text(@data["count"].to_s)
end
end
end
def forward_event(name)
"event.preventDefault(); live.forwardEvent(#{JSON.dump(@id)}, event, {name: #{name.inspect}})"
end
def render(builder)
builder.tag(:div, class: "counter-container") do
builder.tag(:h2) { builder.text("Live Counter") }
builder.tag(:div, id: "counter", class: "counter-display") do
builder.text(@count.to_s)
end
builder.tag(:div, class: "counter-buttons") do
builder.tag(:button, onclick: forward_event("decrement")) do
builder.text("-")
end
builder.tag(:button, onclick: forward_event("increment")) do
builder.text("+")
end
end
end
end
end
Key Points:
- Define
forward_event
(orforward_click
, etc) methods to generate JavaScript for event handling. live.forwardEvent(...)
on the client side invokeshandle(event)
on the server side.- Update the DOM in response to user actions.
- Maintain persistent state in
@data
.