keen_phoenix_svelte
Auto-mount compiled Svelte apps into Phoenix — on both LiveView and plain controller-rendered pages — as self-contained islands.
Write this in a template:
<.svelte name="like" id={"like-#{@id}"} props={%{id: @id, liked: @liked}} />
…and the compiled Svelte app in assets/apps/like/ is mounted into that element,
kept in sync with server state, given a context/api/channel/live bridge to
the server (plus a bus for island-to-island messaging), and torn down on
navigation — no manual <script>/<link> wiring. A configurable placeholder
shows while the bundle loads (server-wide default, per-app <:placeholder>
override), so there's no flash of empty container.
Svelte is the first-class, tooled path, but the mount boundary is
framework-neutral — an island's entry just default-exports
(target, opts) => { setProps, destroy }. Because every island mounts through
that one contract, there's a single component, <.app> (an optional
framework="…" attribute just tags data-framework for debugging). The demo
mounts Svelte, Lit, React and vanilla-JS islands through it. <.svelte> remains
as a back-compat alias.
New here, or comparing this to
live_svelte? Start with Philosophy & comparison — the autonomous-island model, what this deliberately doesn't do, and how it differs fromlive_svelte.
What's New in v1.0.0-rc.2
- Framework-neutral
<.app>— one component mounts any island — Every island mounts through the same(target, opts) => { setProps, destroy }contract, so there is now a singleapp/1component regardless of whether the bundle is Svelte, Lit, React, or hand-written vanilla JS. An optionalframeworkattribute just emits an informationaldata-frameworktag.<.svelte>remains as a back-compat alias for the rc.1 name. - Placeholder / loader — no flash of empty container —
<.app>renders a placeholder that the client clears the instant the bundle mounts (after the fetch, so it stays visible the whole time). Resolution is a per-call<:placeholder>slot › the server-wideconfig :keen_phoenix_svelte, :placeholder› a built-in, dependency-free skeleton. The server default accepts an HTML string, a function,{mod, fun}, orfalseto disable. - Event bus — island-to-island messaging without the server — A
busjoins the app boundary: a page-wide, client-side pub/sub (emit/on/once) over a DOMEventTarget, wired into both the LiveView hook andmountStatic(), so independent islands coordinate without a round-trip and without importing each other. - External apps — a registry with
:direct/:proxydelivery —KeenPhoenixSvelte.Appsregisters islands whose bundle lives elsewhere (a CDN, another deploy);<.runtime>emits aname → urlmanifest andAppsManager.resolve/1loads them. Pick:direct(browser imports the CDN URL) or:proxy(Phoenix fetches and re-serves same-origin viaKeenPhoenixSvelte.Apps.Proxy— no CORS, CSP'self'). - Example reworked into "KeenSpace" — the demo is now a Teams-style workspace that doubles as reference code: chat over channel + Presence, a video catalogue over the REST helper, a calendar over
context.tokens, a bus-driven activity toast, simulated i18n, a plain (non-LiveView) route, and Lit/React/vanilla islands alongside the Svelte ones.
What's New in v1.0.0-rc.1
- Islands for Phoenix —
<.svelte>mounts compiled Svelte apps with zero per-page wiring — The initial release ships the core mounting path: a<.svelte name id props>function component renders a hook-bound<div>(phx-update="ignore",data-app, JSONdata-props), and theKeenSvelteLiveView hook mounts the app onmounted(), pushes prop changes onupdated(), and tears it down ondestroyed().AppsManagerlazilyimport()s/apps/<name>/main.mjsand caches it, so only the bundles actually present on a page are fetched — no manual<script>/<link>tags per app. - One component, two transports — LiveView socket or plain-page REST — Apps mount identically inside a LiveView or on a plain controller-rendered page.
mountStatic()scans[data-app]on non-LiveView pages (skipping[data-phx-session]) and mounts withlive: null, so the same app talks overlive.pushEventwhen a socket is present and falls back to theapiREST helper when it isn't. - A standardized app boundary —
props,context,live,api,channel,bus— Every app's entry receives(target, { props, context, live, api, channel, bus, el }) => handle.context(emitted once per page by<KeenPhoenixSvelte.runtime>) carries user/csrf/tokens/api_base/socket;livebridgespushEvent/handleEvent(with automatic subscription cleanup)/upload;apiattachesx-csrf-token+ session cookie to REST calls;channelis a promise-based, envelope-agnostic Phoenix channel factory with autocidcorrelation; andbusis a page-wide client-side event bus (emit/on/once) for island-to-island messaging that works identically with or without LiveView. - Svelte-version-agnostic mount contract — The mount handle is
{ setProps, destroy }, so the hook drives Svelte 5 (mount/unmount+$state) and transparently falls back to$set/$destroyon Svelte 4. Prop updates are diffed inupdated()to skip redundant re-renders whendata-propsis unchanged. - Dual package — Hex library + bundled npm package — Ships as both
keen_phoenix_svelteon Hex and@keenmate/phoenix_svelteon npm, released in lockstep at the same version. A Vite config helper (@keenmate/phoenix_svelte/vite) builds one self-contained ES module per app with CSS injected by JS. A runnable Phoenix 1.8 demo (thelikeapp on both a LiveView route and a plain route, plus a channel) lives inexample/.
How it works
Two cooperating halves:
- Elixir —
<.svelte>renders a hook-bound<div>(phx-update="ignore",data-app, JSONdata-props);<KeenPhoenixSvelte.runtime>emits the page context. - JS — the
KeenSveltehook mounts the app inside a LiveView andAppsManagerlazilyimport()s/apps/<name>/main.mjs;mountStatic()mounts apps on plain pages. Only the bundles on a page are fetched.
phx-update="ignore" keeps LiveView out of the Svelte-owned subtree; the hook
drives mount / prop-update / teardown across live navigation.
Unlike live_svelte, this is the island model — no ~V sigil, no
server-rendered slots, no SSR Node runtime.
Install
{:keen_phoenix_svelte, "~> 1.0"}
Plus the npm package @keenmate/phoenix_svelte and a Svelte/Vite toolchain. Full
steps in Installation & setup.
Documentation
- Installation & setup — wire the library into your app
- Authoring apps — folder layout, the mount contract, Svelte 5 & 4
- Server communication — runtime context,
live,api,channel,bus - External apps (CDN, direct vs proxy) — load islands from elsewhere, and the
:direct/:proxydelivery modes
A complete, runnable demo (the like app on a LiveView route and a plain route,
plus a channel) lives in the
example/ app.
Versioning
keen_phoenix_svelte is a dual package: the Hex library keen_phoenix_svelte
and the npm package @keenmate/phoenix_svelte are released in lockstep at the
same version — install matching versions of both. The Elixir side renders the
component + hook wiring; the npm side supplies the client runtime (the KeenSvelte
hook, AppsManager, the api/channel helpers, and the Vite build helper).
License
MIT.