howdy/ui/live
Lustre server components over howdy WebSockets.
A server component is a Lustre app whose init, update and view run
on the server. The browser holds a thin client that sends events up and
applies DOM patches that come back. howdy already has the socket, so the
bridge is one route:
import howdy/ui/live
controller.new("/counter")
|> controller.get("/", fn(ctx) { live.serve(ctx, counter.app(), with: 0) })
and one element in a page rendered with howdy/ui/page:
page.new("Counter")
|> page.live
|> page.body([live.mount("/counter")])
|> page.respond(ctx)
The socket route goes through guards and middleware like any other, so
authentication works the same way as for howdy/websocket: read a cookie
or query parameter, since browsers cannot set handshake headers.
Each connection gets its own runtime, started when the socket opens and
shut down when it closes. For one runtime shared by every client, such
as a dashboard, use start and serve_shared.
Views are styled with howdy/ui components automatically. A component
carries the CSS for the classes its view uses, so it is styled whether
or not the page links a stylesheet.
Live links
A page can mount one outlet in place of a mount. A link then swaps
the outlet to another socket route and pushes the page URL onto the
history, so moving between live pages needs no reload:
page.new("Orders")
|> page.live
|> page.body([
live.link(to: "/", mount: "/live/home", children: [text("Home")]),
live.link(to: "/orders", mount: "/live/orders", children: [text("Orders")]),
live.outlet("/live/orders"),
])
The href must serve the full page with the same outlet, because it is
what a reload, a bookmark, a new tab or a browser without JavaScript
loads.
Values
pub fn dispatch(
runtime: lustre.Runtime(msg),
message: msg,
) -> Nil
Send a message to a runtime’s update function from anywhere.
pub fn link(
to href: String,
mount mount: String,
children children: List(element.Element(msg)),
) -> element.Element(msg)
A link that swaps the page’s outlet to the socket at mount and
shows href in the address bar, instead of loading href. Back and
forward swap the outlet too.
It is a plain link wherever swapping is not possible: with a modifier
key or middle click, when the page has no outlet, or before scripts
run. So href must serve a full page whose outlet mounts mount.
Works in pages and in live views.
pub fn mount(route: String) -> element.Element(msg)
The <lustre-server-component> element that connects to route. The
page must include the client runtime; page.live does that.
pub fn navigate(
to href: String,
mount mount: String,
) -> List(attribute.Attribute(msg))
The attributes that make any <a> a live link, for links styled some
other way. See link.
pub fn on_value(
name: String,
message: fn(String) -> msg,
) -> attribute.Attribute(msg)
Hear the value of a form control in a live view: a howdy/ui/select,
combobox or calendar, which keep their value in a hidden input called
name, or any native input, select or textarea with that name. Put it
on an element that contains the control; the change event bubbles to
it and the browser sends the value along.
html.div([live.on_value("plan", PlanChosen)], [
ui.select(id: "plan", name: "plan", ...),
])
pub fn on_values(
name: String,
message: fn(List(String)) -> msg,
) -> attribute.Attribute(msg)
Hear every value of a control that holds several, such as
howdy/ui/command.multiple_combobox called name, each time the set
changes.
pub fn outlet(route: String) -> element.Element(msg)
A mount that live links swap. When a link is clicked the outlet
connects to the link’s socket route and replaces its contents once the
new view arrives. A page has at most one outlet.
pub fn script() -> element.Element(msg)
The script that makes link work. page.live includes it; add it
yourself only when rendering the document some other way, after
server_component.script().
pub fn serve(
ctx: context.Context(guarded),
app: lustre.App(args, model, msg),
with args: args,
) -> response.Response(content.Content)
Upgrade the request to a socket running a fresh runtime of app,
started with args. Equivalent to socket(app, args) followed by
websocket.upgrade(ctx).
pub fn serve_shared(
ctx: context.Context(guarded),
runtime: lustre.Runtime(msg),
) -> response.Response(content.Content)
Upgrade the request to a socket attached to a runtime from start.
Closing the socket detaches it and leaves the runtime running.
pub fn socket(
app: lustre.App(args, model, msg),
args: args,
) -> websocket.Builder(
lustre.Runtime(msg),
server_component.ClientMessage(msg),
)
The socket description serve uses, for adding your own callbacks
before upgrading. The socket state is the runtime.
pub fn socket_shared(
runtime: lustre.Runtime(msg),
) -> websocket.Builder(
lustre.Runtime(msg),
server_component.ClientMessage(msg),
)
The socket description serve_shared uses.
pub fn start(
app: lustre.App(args, model, msg),
with args: args,
) -> Result(lustre.Runtime(msg), lustre.Error)
Start a runtime that outlives any one connection. Every socket served
with serve_shared sees the same model.
pub fn title(content: String) -> element.Element(msg)
Name the document from a live view. When the outlet mounts a view that
contains a title, the document title changes to match, so history
entries are named after the page they lead to. Put it anywhere in the
view; browsers do not display a <title> in the body.