LiveSvelte frontend

Copy Markdown View Source

mix caravela.gen.live MyApp.Domains.Library generates a working frontend for every entity in the domain:

lib/my_app_web/live/library/book_live/{index,show,form}.ex
assets/svelte/library/{BookIndex,BookShow,BookForm}.svelte
assets/svelte/types/library.ts

Per entity you get a trio of Phoenix LiveViews — index (list + delete + "new"), show (single record), form (create/edit with validate round-trips) — each mounting a typed Svelte component via LiveSvelte. One TypeScript file per domain holds the entity interfaces, imported by every component.

Setup

Add LiveSvelte to the consumer app:

# mix.exs
{:live_svelte, "~> 0.19"}

Then follow the LiveSvelte docs to wire it into assets/js/app.js and esbuild/vite.

After generation, the mix task prints a router snippet for you to paste:

scope "/library", MyAppWeb do
  pipe_through :browser

  live "/books", BookLive.Index, :index
  live "/books/new", BookLive.Form, :new
  live "/books/:id", BookLive.Show, :show
  live "/books/:id/edit", BookLive.Form, :edit
end

What the generated LiveViews look like

Each LiveView is standard Phoenix — plain mount/3, explicit handle_event clauses, the context module aliased at the top. No Caravela runtime coupling in the default output, which means you can delete the :caravela dep and the LiveView still compiles and works.

defmodule MyAppWeb.Library.BookLive.Index do
  use MyAppWeb, :live_view
  alias MyApp.Library

  def mount(_params, _session, socket) do
    context = build_context(socket)
    {:ok, assign(socket, books: Library.list_books(context), ...)}
  end

  def handle_event("delete", %{"id" => id}, socket) do
    # … delegate to Library.delete_book/2 …
  end

  def render(assigns) do
    ~H"""
    <LiveSvelte.svelte
      name="library/BookIndex"
      props={%{books: @books, loading: @loading, flash_message: @flash_message}}
      socket={@socket}
    />
    """
  end
end

Version + multi-tenant flow-through

  • Versioned domain → MyAppWeb.V1.Library.BookLive.Index under lib/my_app_web/live/v1/…, mounting v1/library/BookIndex.svelte.
  • Multi-tenant domain → generated build_context/1 picks up socket.assigns[:tenant] automatically, so tenant scoping in the context Just Works.

The --with-domain flag

For an onramp to the more ambitious Live runtime, pass --with-domain:

mix caravela.gen.live --with-domain MyApp.Domains.Library

This emits one extra file per entity — a Caravela.Live.Domain-backed companion module — and regenerates form.ex to use Caravela.Live.Template. Index and show stay plain. The Template-backed form is ~30% shorter and makes the Updater model concrete.

Client events

Svelte components receive pushEvent as a prop and call it to send events back. The generated components use these event names:

ComponentEventParamsWhat the server does
Indexnew{}push_navigate to /new
Indexedit{id}push_navigate to /<id>/edit
Indexdelete{id}Library.delete_book/2 + refetch list
Showedit{id}push_navigate to /<id>/edit
Showback{}push_navigate to index
Formvalidate{field, value}Rebuild changeset, update :errors
Formsave{}create_* or update_*, navigate
Formcancel{}push_navigate to index

Feel free to add your own — generated handle_event clauses live above the # --- CUSTOM --- marker; anything you add below it survives regeneration (see regeneration).

Props your Svelte component sees

For BookIndex.svelte:

let {
  books = [],
  loading = false,
  flash_message = null,
  pushEvent
}: {
  books?: Book[];
  loading?: boolean;
  flash_message?: string | null;
  pushEvent: (event: string, payload: object) => void;
} = $props();

For BookForm.svelte:

let {
  book = {},
  errors = {},
  saving = false,
  pushEvent
}: {
  book?: Partial<Book>;
  errors?: Record<string, string[]>;
  saving?: boolean;
  pushEvent: (event: string, payload: object) => void;
} = $props();

The TypeScript interfaces are regenerated from the domain IR — changes to field :title, :string, required: true in Elixir immediately flow to title: string (no ?) in TypeScript on the next mix caravela.gen.live.

Known incompatibilities with LiveSvelte SSR

LiveSvelte renders components server-side via Node by default. A few Svelte libraries load code lazily at runtime in a way the Node SSR bridge can't resolve:

  • Shiki (syntax highlighter) — dynamically imports per-language grammar chunks. Under Node SSR the dynamic imports fail and the render crashes. Workaround: disable SSR in your app config:

    # config/config.exs
    config :live_svelte, ssr: false

    Client-side hydration still works; only the initial server render falls back to a blank placeholder until the Svelte runtime takes over.

If you hit a similar "works in the browser, crashes in SSR" pattern with another library, the same switch applies.