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.

✨ Search Document