# `KeenPhoenixSvelte`
[🔗](https://github.com/KeenMate/keen-phoenix-svelte/blob/v1.0.0-rc.9/lib/keen_phoenix_svelte.ex#L1)

Auto-mount compiled client apps ("islands") inside Phoenix — LiveView or plain
pages.

Svelte is the first-class, tooled path (hence the name), but the mount boundary
is **framework-neutral**: any island whose JS entry default-exports
`(target, { props, context, live, api, channel, bus, el }) => { setProps, destroy }`
works — Svelte, Lit, React, or hand-written vanilla JS. Because every island
mounts through that one contract, there is a **single** component, `app/1` — the
framework makes no difference to how it's rendered or mounted.

This library ships two cooperating halves:

  * an Elixir **function component** (`app/1`) that renders a placeholder `<div>`
    carrying a LiveView hook, and
  * a JavaScript **hook + `AppsManager`** (published as the npm package
    `@keenmate/phoenix_svelte`) that lazily imports the compiled bundle for
    the requested app and mounts it into that div.

## Why a hook (and not `data-app` + `window.load`)

LiveView owns the DOM. A component mounted by hand on `window.load` fights
morphdom on the next patch and never re-mounts across live navigation. The
hook instead:

  * uses `phx-update="ignore"` so LiveView never touches the Svelte-owned
    subtree,
  * mounts in `mounted()`, pushes fresh props via `$set` in `updated()`, and
    tears the component down in `destroyed()`,
  * hands the component a `live` object (`pushEvent`, `handleEvent`, ...) so it
    talks to the server over the existing LiveView socket.

## Usage

    <.app name="like" id={"like-#{@id}"} props={%{id: @id, liked: @liked}} />

The `id` must be unique and stable (required by LiveView hooks). Props are
JSON-encoded into a data attribute and parsed back on the client, so nested
maps, numbers and booleans survive the trip.

# `app`

Renders a mount point for the compiled client app ("island") named `name`.

This is the one and only island component. It is **framework-neutral**: the
bundle can be Svelte, Lit, React or vanilla JS, and it is rendered and mounted
identically regardless — the framework never changes the wiring.

## Attributes

  * `name` (required) - the app folder name under `assets/apps/`, resolved at
    runtime to `/apps/<name>/main.mjs` (or a registered/CDN URL — see
    `KeenPhoenixSvelte.Apps`).
  * `id` (required) - unique, stable DOM id (LiveView hooks require it).
  * `props` - a map passed to the app. Defaults to `%{}`.
  * `component` - selects which view a multi-component app renders. Pure sugar
    for a `component` key in `props` (see below). Defaults to `nil` (unset).
  * `class` - optional class list on the wrapper div.
  * `tag` - wrapper element, defaults to `"div"`.
  * `eager` - mount **before** the LiveView socket connects. Defaults to `false`.

Any other attribute (e.g. `data-*`, `style`) is forwarded to the wrapper via
the `:global` attribute.

## Selecting a component (multi-component apps)

Independent apps each bundle their own framework runtime. When several views
belong together, ship them as **one** app (one bundle, one shared runtime) and
let each `<.app>` pick a view with `component`:

    <.app name="widgets" id="w-chart" component="chart" props={%{series: @series}} />
    <.app name="widgets" id="w-table" component="table" props={%{rows: @rows}} />

Both mount from the single `widgets` bundle — imported once, mounted twice — so
the runtime is paid once no matter how many views the page shows.

`component` is **exactly** sugar for a `component` entry in `props`: the two
calls below are identical. It wins over a `:component` key already in `props`.

    <.app name="widgets" id="w" component="table" props={%{rows: @rows}} />
    <.app name="widgets" id="w" props={%{component: "table", rows: @rows}} />

The island's entry reads `props.component` and mounts the matching view:

    const VIEWS = { chart: Chart, table: Table };
    export default (target, { props, ...rest }) => {
      const Comp = VIEWS[props.component] ?? Chart;
      // ...mount Comp...
    };

## Eager mounting

By default, on a LiveView page the island mounts from the `KeenApp` hook's
`mounted()` — which cannot run until the socket connects and the view mounts.
On a cold first load that connect round-trip is often the biggest slice of the
delay between "bundle loaded" and "first paint".

With `eager={true}`, the island is mounted immediately by `mountStatic()` (as
the deferred `app.js` parses), the same path a plain page uses — so it paints
without waiting for the socket. It mounts with `live: null` and a boundary
field `liveStatus: "pending"`; once the socket connects, the hook hands it the
live bridge and dispatches a `keen:live-ready` event on the wrapper element.

Use it for islands that don't need `live` to render their first frame — e.g.
ones that fetch from `api`, a `channel`, or a different server entirely. An
island that *must* have `live` to paint should stay on the default (non-eager)
path. On a plain page `eager` is a no-op (there is never a `live` there;
`liveStatus` is `"none"`). See the guide *External apps → First render*.

## Placeholder / loader (no flash of empty container)

Until the compiled bundle is fetched and mounted, the wrapper would otherwise
be empty. To avoid that flash, the wrapper is rendered with a **placeholder**
that the client clears the instant it mounts the app (after the bundle loads,
so it stays visible for the whole fetch). Because the wrapper is
`phx-update="ignore"`, the placeholder is rendered once and never re-diffed.

Resolution, most specific first:

  1. A `<:placeholder>` slot on this call — full HEEx, overrides everything.
     An empty slot (`<:placeholder />`) disables the placeholder for this app.
  2. The server-wide default, `config :keen_phoenix_svelte, :placeholder`.
  3. A built-in, dependency-free skeleton (used when nothing is configured).

Configure the server-wide default once (e.g. in `config/config.exs`):

    # raw HTML string
    config :keen_phoenix_svelte, placeholder: ~s(<div class="my-skeleton"></div>)

    # or a function (1-arity gets the app name), or false to disable globally
    config :keen_phoenix_svelte, placeholder: &MyApp.app_loader/1
    config :keen_phoenix_svelte, placeholder: false

Per app, override with the slot:

    <.app name="chart" id="chart" props={@cfg}>
      <:placeholder>
        <div class="skeleton h-64 w-full"></div>
      </:placeholder>
    </.app>

## Attributes

* `name` (`:string`) (required)
* `id` (`:string`) (required)
* `props` (`:map`) - Defaults to `%{}`.
* `component` (`:string`) - Selects a view in a multi-component app; sugar for a `component` key in `props`. Defaults to `nil`.
* `class` (`:any`) - Defaults to `nil`.
* `tag` (`:string`) - Defaults to `"div"`.
* `eager` (`:boolean`) - Defaults to `false`.
* Global attributes are accepted.
## Slots

* `placeholder` - Markup shown until the island mounts. Overrides the server-wide default.

# `runtime`

Emits the page-wide runtime context, read once by the client and injected into
every mounted app as `context`.

Render it once per page (e.g. in your root layout), on both LiveView and plain
controller-rendered pages:

    <KeenPhoenixSvelte.runtime context={%{
      user: %{id: @current_user.id, name: @current_user.name, roles: @roles},
      csrf_token: get_csrf_token(),
      api_base: "/api"
    }} />

Keep this to *context*, not payload — user identity, a CSRF token for the `api`
helper, an `api_base`, locale, and any tokens the app needs. Per-app data
belongs in each `<.app props={...} />`.

The context is JSON-encoded with HTML-safe escaping so it is safe to embed in
the `<script type="application/json">` tag.

## Preloading bundles

An island's bundle is normally fetched *lazily* by the client — on a LiveView
page that `import()` doesn't fire until the socket connects and the hook mounts,
so the download starts hundreds of ms into the page. `preload` has the browser
fetch the bundles **during initial HTML parse** instead, via
`<link rel="modulepreload">`, so the bytes are cached by the time the hook runs
(the render still waits on mount — you're only moving the *download* earlier).

By default this is **automatic**: every `<.app>` rendered on the page records
itself, and `<.runtime>` preloads exactly those bundles — no per-page list to
maintain, and nothing preloaded that the page doesn't mount:

    <KeenPhoenixSvelte.runtime context={@ctx} />

Auto-detection works because the page body (where the `<.app>` tags live) is
rendered *before* the root layout's `<head>` (where `<.runtime>` goes), so the
runtime already knows which islands the page mounted. Keep `<.runtime>` in a
layout that **wraps** the page content (the standard root-layout placement) — if
it renders before the `<.app>` tags, auto sees nothing and preloads nothing
(harmless: it just falls back to lazy loading).

`preload` accepts:

  * `:auto` (default) — the apps actually rendered on this page. The right choice
    almost always; scopes to exactly what's on the page with no manual list.
  * a **list of app names** — preload exactly those. Use it to override auto
    (e.g. preload an app a later interaction will mount). Registered apps use
    their manifest URL; a local app falls back to `base_path/<name>/main.mjs`.
  * `true` — preload every app in the manifest, whether or not it's on this page.
    Rarely what you want; auto is page-scoped and needs no registry.
  * `false` — emit nothing (opt back into fully lazy loading).

A cross-origin (`:direct`) URL gets `crossorigin="anonymous"` so the preload's
credentials mode matches the module `import()` and the fetch is actually reused.

Only the **entry** bundle is preloaded. A multi-file island's sibling assets
(a stylesheet or data file it pulls via `new URL("./x.css", import.meta.url)`) are
discovered *inside* the bundle at mount time, so the server can't know their URLs
to preload them. If a sibling's early load matters, inline it into the JS
(single-file island) or add your own `<link rel="preload">` for a known filename.

## Attributes

* `context` (`:map`) - Defaults to `%{}`.
* `id` (`:string`) - Defaults to `"keen-context"`.
* `preload` (`:any`) - Emit <link rel=modulepreload> for app bundles: `:auto` (default — the apps actually rendered on this page), a list of app names, `true` (all manifest apps), or `false` (none). Defaults to `:auto`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
