-
Notifications
You must be signed in to change notification settings - Fork 1
LiveView
Phoenix.LiveView-shape server-rendered stateful UI over
WebSocket. Subclass Tep::LiveView, override render /
handle_event / optionally handle_presence_diff; the
framework handles the WS lifecycle + client-side bootstrap.
Battery 4 in docs/BATTERIES-DESIGN.md.
require 'sinatra'
set :scheduler, :scheduled # WS requires the scheduled server
class CounterView < Tep::LiveView
def initialize
super
@count = 0
end
def topic
"counter:global"
end
def render
"<div id='tep-live-root'>" +
"Count: " + @count.to_s +
" <button data-event='inc'>+</button>" +
" <button data-event='dec'>-</button>" +
"</div>"
end
def handle_event(event, payload, req)
if event == "inc"
@count += 1
elsif event == "dec"
@count -= 1
end
broadcast_render
0
end
end
# Shared instance — all viewers see the same counter:
COUNTER = CounterView.new
get '/counter' do
COUNTER.mount(req)
Tep::LiveView.render_page(COUNTER.render, "/counter_live")
end
websocket "/counter_live" do |ws|
on_open do |evt|
Tep::Broadcast.subscribe_ws(COUNTER.topic, ws.fd)
ws.text(COUNTER.render)
end
on_message do |evt|
COUNTER.dispatch_event_json(evt.data, req)
# broadcast_render inside handle_event already sent the new
# HTML to every WS subscribed to COUNTER.topic.
end
on_close do |evt|
Tep::Broadcast.unsubscribe_fd(ws.fd)
end
endThat's a working LiveView. Open /counter in two browsers,
click + in one, both update.
-
Initial GET → server renders the view + ships the
bootstrap HTML page (
render_page). -
Client opens WS to the supplied path.
on_opensends the initial render. -
Client click on
[data-event="X"]→ bootstrap JS sends{"event":"X","payload":"<data-payload>"}over the WS. -
Server
on_message→dispatch_event_jsonparses + callshandle_event→ view mutates state →broadcast_renderpublishes the new HTML to everyone subscribed. -
Client receives the new HTML; bootstrap replaces
#tep-live-root'souterHTMLwith it. Done.
# Subclass these:
def mount(req) # seed @ivars from req
def render # build HTML
def handle_event(event, payload, req) # mutate state
def topic # broadcast binding (default "")
def handle_presence_diff(diff_json) # react to Tep::Presence events
# Inherited imeths apps call:
view.dispatch_event_json(json, req) # bridge from WS message
view.apply_presence_diff_json(json) # bridge from presence diff stream
view.broadcast_render # publish self.render to self.topic
# Cmeths:
Tep::LiveView.render_page(content, ws_path) # initial-page wrapperTep::LiveView.render_page emits ~10 lines of inline JS that:
- Open a WebSocket to
ws_path. - On each incoming text frame: parse as HTML, replace
#tep-live-root'souterHTML. - Intercept clicks on any
[data-event]element, send{"event": <data-event>, "payload": <data-payload or "">}over the WS.
That's all v1 ships. No morphdom, no form submission, no key bindings. "Click + re-render" is enough to demonstrate the pattern. Morphdom-style client-side diff lands later if wire size becomes meaningful.
view.topic returns the broadcast topic this view binds to.
Default "" (no binding). Subclasses override:
def topic
"counter:" + @id.to_s
endWhen non-empty, broadcast_render publishes self.render to
self.topic via Tep::Broadcast. All WSes subscribed
to that topic (including the originating client) receive the
new HTML.
For per-instance state (a separate counter per :id), each WS
connection needs its own view instance — apps write the
manual wiring as shown above.
view.handle_presence_diff(diff_json) is the callback for
Tep::Presence events on the view's topic.
Subclasses override; default is no-op.
Apps wire the diff stream into their on_message block, OR (when
matz/spinel#641 lands)
the framework auto-installs a per-WS background fiber that
subscribes to Tep::Presence.diff_topic(topic) and dispatches
to apply_presence_diff_json.
The diff is flat JSON with kind / principal / ekind /
agent_id / state / note / until_ts fields — same shape
Tep::Presence emits.
-
Tep.live "/path", CounterViewauto-routing. Apps wire two routes manually. Auto-wire needs translator changes or a spinel mechanism to bridge runtime class instantiation without closure captures. -
Server-side
handle_broadcast(msg)intercept that triggers re-render on broadcast receipt. Needs a per-WS background fiber, gated on matz/spinel#641. -
Diff-on-client via morphdom. v1 ships full
outerHTMLreplacement. Future chunk. -
Form submissions / multi-arg events. v1 carries one
payloadstring. Apps stringify their own JSON for richer payloads.
-
Spinel:
dispatch_event_jsonis an imeth. Class methods taking aTep::LiveViewparam widen tosp_RbVal(poly) when called across subclasses, and spinel doesn't auto-box concrete subclass pointers into the poly slot at the call site. The imeth dispatches through the subclass instance's typed slot, sidestepping the box. -
Topic-less views are valid — just a single-viewer
LiveView with
rendersent onon_message. The framework doesn't require broadcast. -
Each WS needs its own view instance if the LiveView has
per-user state. Apps construct fresh instances in their
on_openblock. Shared-state LiveViews (a global counter, a chatroom) use a single shared instance (see the counter example above).