Livery¶
A library for building interactive, server-rendered LiveView UIs over WebSocket.
Define server-side view logic by implementing the LiveView trait:
mountinitializes state on theSockethandle_eventresponds to client interactionshandle_inforeceives server-push messages from external actorsrenderproduces HTML from the currentAssigns
Use HtmlTemplate from the templates library for rendering — it auto-escapes
dynamic values by default.
Components¶
Compose UIs from stateful LiveComponent instances embedded within a
LiveView. Each component has its own assigns, lifecycle, and event handling.
Register components through Socket:
Socket.register_component(id, component)— register and mount a componentSocket.update_component(id, data)— pass data from the parent to a componentSocket.unregister_component(id)— remove a component
Components render independently through HtmlTemplate. The parent accesses
component output in render via assigns.component_html(id) and inserts it
as unescaped HTML (safe because the component's own template already escaped
all dynamic values). Use assigns.render_values() to create a writable child
scope of the template values for overlaying component HTML.
Target events to specific components with the lv-target attribute in HTML.
Events without lv-target route to the parent LiveView.
Stateless components are a convention, not a framework feature — just primitives or classes with a render function that takes data and returns HTML.
Server Push¶
External actors can send messages to a connection through PubSub or
directly via InfoReceiver. Messages arrive at LiveView.handle_info,
where the view can update assigns and trigger a re-render.
- Call
Socket.self()in a lifecycle method to get a shareableInfoReceiverhandle - Call
Socket.subscribe(topic)to receive messages from a PubSub topic - Subscriptions are automatically cleaned up when the connection closes
Forms¶
Form handling works through the existing handle_event API — no additional
library types are needed. The JavaScript client sends form field data as a
JSON object payload via lv-change (fires on every keystroke for real-time
validation) and lv-submit (fires on form submission).
On the server, extract fields with JsonNav and validate:
fun ref handle_event(event: String val, payload: json.JsonValue,
socket: Socket ref)
=>
let nav = json.JsonNav(payload)
try
let username = nav("username").as_string()?
let email = nav("email").as_string()?
// validate and assign errors
end
Store field values and error messages as assigns so the template renders both the current input values and per-field feedback.
Server-Rendered First Paint¶
Eliminate the empty-page flash on initial load by rendering the LiveView to HTML at HTTP request time. The browser receives a fully populated page, then the JS client silently takes over when the WebSocket connects.
Use PageRenderer to render a view without a WebSocket connection:
let factory: Factory = {(): LiveView ref^ ? => MyView?} val
match PageRenderer.render(factory)
| let html: String val =>
// Embed html in the HTTP response inside the lv-root container
| let err: PageRenderFactoryFailed =>
// Factory failed to create the view
| let err: PageRenderFailed =>
// View's render method failed
end
The rendered view sees a disconnected socket — connected() returns false,
PubSub operations are no-ops, and push events are silently dropped. Check
socket.connected() in mount to vary behavior between HTTP render and
WebSocket.
When the JS client opens the WebSocket, the server mounts a fresh view (producing identical initial HTML), and morphdom silently patches the pre-rendered DOM with no visible change.
Split Rendering¶
By default, the framework sends the full HTML string on every re-render.
For views with large templates where only a few values change between
renders, override render_parts to enable split rendering — the framework
sends static template parts once per connection and only changed dynamic
slot values on subsequent renders.
fun box render_parts(assigns: Assigns box,
sink: templates.TemplateSink ref): Bool
=>
try
_template.render_to(sink, assigns.template_values())?
true
else
false
end
When render_parts returns true, the framework uses the split wire
protocol (render_full on first render, render_diff on subsequent
renders with changes). When it returns false (the default), the
framework falls back to render() and sends full HTML. Both paths can
coexist — a view that implements both methods gets split rendering over
WebSocket while PageRenderer continues to use render() for HTTP.
Getting Started¶
- Implement the
LiveViewtrait on a class - Register routes via
Routerand freeze withRouter.build() - Create a
PubSubinstance - Start a
Listenerwith your routes and PubSub
See the examples directory for working applications.