Caravela.Live.Updater (Caravela v0.6.0)

Copy Markdown View Source

Composable state updater functions for LiveView assigns.

An updater is a pure function from one assigns-map to another, or a function of arity 2 that takes an extra argument (an "event payload"). The point is to make state transitions composable and testable in isolation — any LiveView handle_event can be reassembled from small named updaters rather than inlining the logic.

When the composed updater is applied via apply/3 to a socket, the LiveView calls assign/2 with the returned map, and LiveSvelte pushes the resulting prop diff to the Svelte component.

Quick example

import Caravela.Live.Updater

mark_saving = fn assigns -> %{assigns | saving: true} end
clear_flash = fn assigns -> %{assigns | flash_message: nil} end

combined = mark_saving ~> clear_flash
combined.(%{saving: false, flash_message: "old"})
#=> %{saving: true, flash_message: nil}

The ~> operator is sugar for compose/2 and can chain any number of updaters into a single function.

Summary

Functions

Compose two updaters: apply a, then apply b to the result.

Narrow an updater so it operates on one nested key of the assigns map, leaving every other key untouched. Useful when a parent LiveView embeds child-domain state under a namespaced key.

Apply an updater to a LiveView socket, assigning the returned map.

Pipe operator for chaining updaters: u ~> v is equivalent to compose(u, v). Left-associative, so a ~> b ~> c composes as compose(compose(a, b), c).

Types

assigns()

@type assigns() :: map()

updater()

@type updater() :: (assigns() -> assigns()) | (assigns(), any() -> assigns())

Functions

compose(a, b)

@spec compose(updater(), updater()) :: (assigns() -> assigns())

Compose two updaters: apply a, then apply b to the result.

If either side takes two arguments (an event payload), use apply/3 with the payload threaded explicitly. compose/2 itself only composes the 1-arity shape — keep 2-arity updaters at the call boundary.

embed(updater, key)

@spec embed(updater(), atom()) :: (assigns() -> assigns())

Narrow an updater so it operates on one nested key of the assigns map, leaving every other key untouched. Useful when a parent LiveView embeds child-domain state under a namespaced key.

increment = fn %{counter: n} = s -> %{s | counter: n + 1} end

scoped = Caravela.Live.Updater.embed(increment, :child_state)
scoped.(%{child_state: %{counter: 0}, other: :untouched})
#=> %{child_state: %{counter: 1}, other: :untouched}

run(socket, updater)

@spec run(map(), updater()) :: map()

Apply an updater to a LiveView socket, assigning the returned map.

Accepts both arities: run(socket, u) for (assigns -> assigns), and run(socket, u, arg) for (assigns, arg -> assigns). The result replaces socket.assigns, which means LiveSvelte re-renders the Svelte component with the new props.

For real Phoenix.LiveView.Socket structs we route through Phoenix.Component.assign/2 so LiveView change-tracking fires. For plain-map sockets (used in tests / tooling) we replace :assigns directly. That keeps Caravela.Live.Updater unit-testable without a live LiveView process.

run(socket, updater, arg)

@spec run(map(), updater(), any()) :: map()

a ~> b

(macro)

Pipe operator for chaining updaters: u ~> v is equivalent to compose(u, v). Left-associative, so a ~> b ~> c composes as compose(compose(a, b), c).

import Caravela.Live.Updater

pipeline = updater_a ~> updater_b ~> updater_c
pipeline.(assigns)