Live Mercury puts Mercury components on pages that Phoenix renders. The server writes one element and tells LiveView to leave it alone; a client hook mounts a Mercury hyperscript component into it, gives it the server's props, and keeps it current as the LiveView's assigns change.
What Live Mercury is
An island is one element on a server-rendered page that a client component owns. Everything around it is HEEx; the island is Mercury hyperscript. LiveView never patches inside it, because the element carries phx-update="ignore". The props travel in the element's attributes, and updates travel as patches over the LiveView socket the page already holds.
So a page reads without JavaScript, the server stays the owner of the data, and only the part that needs a client runs one.
The two halves
| Half | Lives in | What it gives you |
|---|---|---|
| server | echo/apps/live_mercury, module LiveMercury |
mercury/1, the function component that renders the island's element and its props |
| browser | fiberfx/live-mercury, package @fiberfx/live-mercury 4.0.0 |
getHooks and createMercuryHook (the MercuryHook lifecycle), useLiveMercury, Link, and the patch codecs |
@fiberfx/hyperscript is a peer of the browser half, so an island renders with the page's one hyperscript runtime.
The KAN board is both halves at once. Its LiveView renders the island:
def render(assigns) do
~H"""
<EchdOSWeb.KanLayouts.nav active={:board} />
<LiveMercury.mercury
name="KanBoard"
id="kan-board"
socket={@socket}
source={@source}
now={@now}
lanes={@lanes}
rungs={@rungs}
messages={@messages}
/>
"""
end
and its bundle registers the component under the same name and opens the socket:
const liveSocket = new LiveSocket("/live", Socket, {
params: { _csrf_token: csrfToken },
hooks: getHooks({ KanBoard, SeatsExplorer, SquadsExplorer, KanAdmin }) as never,
});
liveSocket.connect();
name="KanBoard" on the server and the KanBoard key in getHooks's map are the one link between them.
Two hosts in this repository
The course follows two real consumers, and they mount islands two ways:
- echd_os's KAN views (
kan_board_live.ex,kan_seats_live.ex,kan_squads_live.ex,kan_admin_live.ex) render<LiveMercury.mercury>. One bundle,kan.mjs, registers four components withgetHooksand opens the page's LiveSocket on/live. - EDU's course islands render
EchoEduWeb.Island.island/1, a host namedCourseIsland. Each island is its own bundle; EDU's boot imports it by URL and hands LiveView's callbacks to the bundle'shook, which is Live Mercury's lifecycle built bycourseIsland(component).
Both end in the same lifecycle, createMercuryHook. They differ in where the props come from and in who imports the component.
The course
- LM0, Islands: what an island is, the element each host renders, and how the client mounts it.
- LM1, An island in a LiveView: the socket path, the hook's four callbacks, and events in both directions.
- LM2, Building islands with mx:
mx build --lib, the five builds behind EDU's islands, and the image stage that has no Node. - LM3, Props and streams:
data-props-diffanddata-streams-diff, the compact patch format,upsert,limitand$$<id>paths. - LM4, Server rendering: the
ssrflag, off by default,LiveMercury.SSR, and why the client replaces server markup instead of hydrating it. - LM5, Testing islands:
LiveMercury.Test.get_mercury/2, testing an island's events, the hook tests, and EDU'scheck_hosts/3. - LM6, the capstone, is planned: an island in a LiveView built with no Node. It waits on epic 24's H3–H5 and on the KAN views' bundle leaving Vite, since
kan.mjsis still built with pnpm and Vite today.
What is shipped
EDU's islands are built by mx in the image, in a stage that checks there is no node on its PATH. The KAN views' bundle, kan.mjs, is not built there: it is committed in echd_os's priv/static and built outside the image, which building shows. @fiberfx/live-mercury is version 4.0.0 in its package.json.