Falcon::RailsGuidesReal-Time Views

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:

When NOT to use Live::View:

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:

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:

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:

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:

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:

Interactive Example: Counter with Buttons

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: