# Svelte frontend (render modes)

`mix caravela.gen.live MyApp.Domains.Library` generates a working
Svelte frontend for every entity in the domain. Each entity picks one
of two render modes via its DSL declaration:

* `frontend: :live` (default) — Phoenix LiveView + WebSocket.
* `frontend: :rest` — Phoenix controller + Inertia-style HTTP.

Both modes mount Svelte components through
[`caravela_svelte`](https://hex.pm/packages/caravela_svelte), so the
same `BookIndex.svelte` works under either transport without
changes — the author barely notices which mode is in play.

## Setup

Add `caravela_svelte` to the consumer app:

```elixir
# mix.exs
{:caravela, "~> 0.13"},
{:caravela_svelte, "~> 0.1"}
```

Wire the client runtime into `assets/js/app.js` following the
[`caravela_svelte` docs](https://hexdocs.pm/caravela_svelte).

## Generated files

```
lib/my_app_web/live/library/book_live/{index,show,form}.ex   # :live mode
lib/my_app_web/controllers/book_controller.ex                # :rest mode
assets/svelte/library/{BookIndex,BookShow,BookForm}.svelte   # both modes
assets/svelte/library/{BookIndex,BookShow,BookForm}.test.ts  # Vitest smoke tests
assets/svelte/types/library.ts                               # shared TS
test/my_app_web/live/library/book_live_test.exs              # :live tests
test/my_app_web/controllers/book_controller_test.exs         # :rest tests
```

The trio of Svelte components is identical between modes. Mode is a
server-side decision; the Svelte component receives the same props
either way.

## Registering routes

Don't paste route snippets. `Caravela.Router` exposes a compile-time
macro that expands to the full route list based on each entity's
`frontend` / `realtime?` declaration:

```elixir
defmodule MyAppWeb.Router do
  use Phoenix.Router
  use Caravela.Router
  import CaravelaSvelte.Router  # needed only when any entity is :rest

  scope "/", MyAppWeb.Library do
    pipe_through :browser
    caravela_routes MyApp.Domains.Library
  end
end
```

`caravela_routes/1` expands into `live` routes for `:live` entities
and `caravela_rest` routes for `:rest` entities, with
`realtime: true` appended automatically when the entity opts in.
Versioned domains (`version "v1"`) insert the version segment into
the module aliases so Phoenix's scope alias resolution continues to
match the generated `MyAppWeb.V1.Library.BookLive.Index` layout.

```elixir
caravela_routes MyApp.Domains.Library,
  session: [on_mount: {MyAppWeb.Auth, :require_user}]
```

Passing `:session` wraps the `:live` routes in a `live_session/3`
block so a group of entities can share an `on_mount` hook.

## `:live` mode — what the generated LiveView looks like

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

  def mount(_params, _session, socket) do
    context = build_context(socket)

    socket =
      socket
      |> assign(:context, context)
      |> assign(:field_access, Library.field_access(:books, context))
      |> assign(:actions, Library.action_access(:books, context))
      |> assign(:books, Library.list_books(context))
      |> assign(:loading, false)
      |> assign(:flash_message, nil)

    {:ok, socket}
  end

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

The 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.

## `:rest` mode — what the generated controller looks like

```elixir
defmodule MyAppWeb.BookController do
  use Phoenix.Controller, formats: [:html]

  alias MyApp.Library
  alias Caravela.ChangesetTranslator
  alias CaravelaSvelte.Caravela, as: CS

  def index(conn, _params) do
    context = build_context(conn)
    items = Library.list_books(context)
    field_access = field_access(context)
    actions = Library.action_access(:books, context)

    conn
    |> CS.put_field_access(field_access)
    |> CaravelaSvelte.render("library/BookIndex", %{
      books: items,
      field_access: field_access,
      actions: actions
    })
  end

  # … show / new / edit / create / update / delete …
end
```

`CaravelaSvelte.render/3` produces an Inertia-compatible HTTP
response (JSON for SPA navigation, full HTML on first load).
Structured changeset errors flow through `Caravela.ChangesetTranslator`
(see [validation](validation.md)).

## `realtime: true` for `:rest` entities

Declaring `realtime: true` on a `:rest` entity:

```elixir
entity :books, frontend: :rest, realtime: true do
  field :title, :string, required: true
end
```

… adds three things to the generated controller:

1. `CS.broadcast_patch/3` on create / update / delete, keyed by the
   conventional per-actor topic `"caravela:<entity>:actor:<id>"`.
2. JSON-Patch operations matching the list mutation (`add` /
   `replace` / `remove` on `/<plural>/<id>`).
3. A matching `caravela_rest "/books", BookController, realtime: true`
   line in the router expansion, which
   [`CaravelaSvelte.Router`](https://hexdocs.pm/caravela_svelte) wires
   into the SSE endpoint clients subscribe through.

`:live` entities already have LiveView's WebSocket for real-time, so
`realtime: true` on a `:live` entity is rejected at compile time with
a clear error.

## Version + multi-tenant flow-through

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

## The `--with-domain` flag (`:live` mode only)

For an onramp to the more ambitious [Live runtime](live_runtime.md),
pass `--with-domain`:

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

This emits one extra file per `:live` 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. `:rest` entities are unaffected.

## Other flags

- `--frontend rest|live` — blanket override. Treats every entity in
  the domain as the given mode, ignoring DSL declarations. Useful
  for previewing generator output.
- `--no-tests` — skip emitting the ExUnit + Vitest test skeletons
  (they're emitted by default — see [testing](testing.md)).
- `--dry-run`, `--force`, `--output DIR` — standard across all
  generators.

## Client events (`:live` mode)

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

| Component  | Event      | Params           | What the server does                   |
|------------|------------|------------------|----------------------------------------|
| Index      | `new`      | `{}`             | `push_navigate` to `/new`              |
| Index      | `edit`     | `{id}`           | `push_navigate` to `/<id>/edit`        |
| Index      | `delete`   | `{id}`           | `Library.delete_book/2` + refetch list |
| Show       | `edit`     | `{id}`           | `push_navigate` to `/<id>/edit`        |
| Show       | `back`     | `{}`             | `push_navigate` to index               |
| Form       | `validate` | `{field, value}` | Rebuild changeset, update `:errors`    |
| Form       | `save`     | `{}`             | `create_*` or `update_*`, navigate     |
| Form       | `cancel`   | `{}`             | `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](regeneration.md)).

## Client navigation (`:rest` mode)

`:rest` pages use Inertia-style SPA navigation through
`caravela_svelte`'s client helpers (`navigate`, `useForm`). See the
[`caravela_svelte` docs](https://hexdocs.pm/caravela_svelte) for the
full client API.

## Props your Svelte component sees

For `BookIndex.svelte`:

```ts
let {
  books = [],
  loading = false,
  flash_message = null,
  field_access = { title: true, isbn: true /* … */ },
  actions = { create: true, update: true, delete: true },
  live  // :live mode only; absent under :rest
}: {
  books?: Book[];
  loading?: boolean;
  flash_message?: string | null;
  field_access?: BookFieldAccess;
  actions?: BookActions;
  live?: LiveHandle;
} = $props();
```

For `BookForm.svelte`:

```ts
let {
  book = {},
  errors = {},
  saving = false,
  field_access = { /* … */ },
  actions = { create: true, update: true, delete: true },
  live
}: {
  book?: Partial<Book>;
  // Structured error shape from Caravela.ChangesetTranslator.
  // See docs/validation.md.
  errors?: Record<string, Array<{ code: string; params: object; message: string }>>;
  saving?: boolean;
  field_access?: BookFieldAccess;
  actions?: BookActions;
  live?: LiveHandle;
} = $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`. See [policies](policies.md) for how
`BookFieldAccess` and `BookActions` relate to the `policy` block.

## SSR caveats

Both modes render components server-side via `caravela_svelte`'s Node
bridge by default. A few Svelte libraries load code lazily at runtime
in a way the Node 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:

  ```elixir
  # config/config.exs
  config :caravela_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.
