keen_phoenix_svelte
π Live demo & docs: keen-phoenix-svelte.keenmate.dev Β· Hex Β· HexDocs
Auto-mount compiled Svelte (React, Lit, β¦) apps into Phoenix β on both LiveView and plain controller-rendered pages β as self-contained islands.
Write this in a template:
<.app 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>. The demo mounts Svelte,
Lit, React and vanilla-JS islands through it.
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.6
- Eager mounting β paint before the socket connects β
<.app eager>mounts an island the instantapp.jsparses instead of waiting for the LiveView hook, collapsing the cold-load gap between the server-rendered placeholder and the live island. It starts withoutliveand is upgraded once the socket connects β ideal for islands that draw fromapi, achannel, or another server. A no-op on plain pages. liveStatuson the boundary β every island now knows whetherliveis here ("ready"), coming ("pending", an eager mount), or never present ("none", a plain page) β enough to show a loader while waiting and choose the right transport.keen:live-readyβ aCustomEvent(withdetail.live), plus an optionalsetLive(live)handle method, fired when an eagerly-mounted island'slivebridge arrives. Server-pushed prop updates keep flowing the whole time; only imperativelive.*calls need the bridge.
What's New in v1.0.0-rc.5
- Automatic, page-scoped preloading β on by default β
<KeenPhoenixSvelte.runtime>now defaults topreload={:auto}: each<.app>records itself as it renders and the runtime emits<link rel="modulepreload">for exactly the islands on that page, so bundles download during initial HTML parse with no per-page list to maintain. A page with no islands preloads nothing; opt out withpreload={false}or override with an explicit list. - Vite helper build fix β the documented published install works β
@keenmate/phoenix_svelte/viteno longer imports the undeclaredsvelte-preprocess(which failed withCannot find package); it usesvitePreprocessfrom the Svelte plugin you already depend on, soappConfigbuilds with just the documented dependencies. - One fewer attribute β
<.app framework="β¦">removed β it only emitted adata-frameworktag the runtime never read, so it was API surface for no behavior. Drop it; a bundle already is whatever framework it is. - Install docs β production build wiring (no more missing bundles) β
installation.mdnow documents the npmscriptsandmixalias wiring (assets.setup/build/deploy) that builds islands formix setupand releases β previouslymix assets.deployshipped none. Plus a clearer app.js edit (which lines you add vs. Phoenix's generated ones) and a note thatstatic_pathsappsis only needed for local apps.
How it works
Two cooperating halves:
- Elixir β
<.app>renders a hook-bound<div>(phx-update="ignore",data-app, JSONdata-props);<KeenPhoenixSvelte.runtime>emits the page context. - JS β the
KeenSveltehook mounts the island inside a LiveView andAppsManagerlazilyimport()s/apps/<name>/main.mjs(or a registered URL);mountStatic()mounts islands on plain pages. Only the bundles on a page are fetched.
Neither half is Svelte-specific: the bundle it mounts can be Svelte, React, Lit,
Vue, or vanilla JS, because every island mounts through the same framework-neutral
(target, opts) => { setProps, destroy } contract. <.app> is the one component for
all of them.
phx-update="ignore" keeps LiveView out of the island's 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.