LiveSvelte provides useLiveForm for building reactive forms backed by Ecto changesets with server-side validation.
Quick Example
LiveView:
defmodule MyAppWeb.UserFormLive do
use MyAppWeb, :live_view
alias MyApp.Accounts
def mount(_params, _session, socket) do
form = to_form(Accounts.change_user(%Accounts.User{}))
{:ok, assign(socket, form: form)}
end
def handle_event("validate", %{"user" => params}, socket) do
form = params |> Accounts.change_user() |> Map.put(:action, :validate) |> to_form()
{:noreply, assign(socket, form: form)}
end
def handle_event("submit", %{"user" => params}, socket) do
case Accounts.create_user(params) do
{:ok, _user} -> {:noreply, push_navigate(socket, to: "/")}
{:error, changeset} -> {:noreply, assign(socket, form: to_form(changeset))}
end
end
def render(assigns) do
~H"""
<.svelte name="UserForm" props={%{form: @form}} socket={@socket} />
"""
end
endSvelte Component:
<!-- assets/svelte/UserForm.svelte -->
<script>
import { useLiveForm } from "live_svelte"
let { form } = $props()
const { field } = useLiveForm(() => form)
</script>
<form phx-submit="submit" phx-change="validate">
<div>
<label>Name</label>
<input {...field("name")} />
{#if field("name").error}
<span class="error">{field("name").error}</span>
{/if}
</div>
<div>
<label>Email</label>
<input type="email" {...field("email")} />
{#if field("email").error}
<span class="error">{field("email").error}</span>
{/if}
</div>
<button type="submit">Save</button>
</form>The useLiveForm Composable
import { useLiveForm } from "live_svelte"
const { field, fieldArray } = useLiveForm(() => form, options?)The first argument is a getter function (not the form value directly). This ensures useLiveForm always reads the latest reactive prop value.
Options
type FormOptions = {
changeEvent?: string // Event name for validation (default: "validate")
submitEvent?: string // Event name for submission (default: "submit")
debounceInMilliseconds?: number // Debounce delay for change events (default: 300)
}The field() Function
field(name) returns an object you can spread onto an <input> element:
<input {...field("email")} />It returns:
name— the HTML input name (matches changeset field)value— current field value from the changeseterror— error message string (ornull)phx-debounce— debounce attribute for change events
You can also access properties individually:
<input
name={field("email").name}
value={field("email").value}
class={field("email").error ? "border-red-500" : ""}
/>
{#if field("email").error}
<p>{field("email").error}</p>
{/if}Nested Fields
Access nested fields with dot notation:
<input {...field("address.street")} />
<input {...field("address.city")} />Dynamic Arrays with fieldArray()
For embeds_many or has_many with nested forms:
<script>
import { useLiveForm } from "live_svelte"
let { form } = $props()
const { field, fieldArray } = useLiveForm(() => form)
const skills = fieldArray("skills")
</script>
{#each skills.fields as skillField, i}
<div>
<input {...field(`skills.${i}.name`)} />
<button type="button" onclick={() => skills.remove(i)}>Remove</button>
</div>
{/each}
<button type="button" onclick={() => skills.append({ name: "" })}>Add Skill</button>fieldArray(path) returns:
fields— reactive array of field descriptorsappend(value)— add an item to the endprepend(value)— add an item to the startremove(index)— remove an item by index
Encoding Changesets as Props
To pass a changeset form as props, use LiveSvelte.Encoder for the changeset data. Phoenix's to_form/1 produces a Phoenix.HTML.Form struct that LiveSvelte can encode automatically.
For custom structs used inside the form data, use @derive:
defmodule MyApp.Address do
@derive {LiveSvelte.Encoder, only: [:street, :city, :zip]}
embedded_schema do
field :street, :string
field :city, :string
field :zip, :string
end
endTypeScript Types
import type { Form } from "live_svelte"
// Type your component props
let { form }: { form: Form<{ name: string; email: string }> } = $props()
const { field } = useLiveForm(() => form)
// field("name").value is typed as stringGettext Integration
If you have a Gettext backend configured, LiveSvelte translates error messages automatically:
# config/config.exs
config :live_svelte, gettext_backend: MyAppWeb.GettextError messages from changesets will use your Gettext translations.
Full Form Example with Validation
# LiveView
def handle_event("validate", %{"user" => params}, socket) do
changeset =
%User{}
|> User.changeset(params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, form: to_form(changeset))}
endSetting action: :validate on the changeset causes Ecto to include validation errors, which LiveSvelte then passes back to the field().error values in the component.