LiveVue
1.0
Nested Objects
Access nested form fields using
dot notation
paths. Perfect for forms with embedded objects like addresses, profiles, or settings.
What this example shows
defmodule MyAppWeb.NestedFormLive do
use MyAppWeb, :live_view
import Ecto.Changeset
defmodule Address do
use Ecto.Schema
import Ecto.Changeset
@derive LiveVue.Encoder
@primary_key false
embedded_schema do
field :street, :string
field :city, :string
field :zip, :string
end
def changeset(address, attrs) do
address
|> cast(attrs, [:street, :city, :zip])
|> validate_required([:street, :city, :zip])
|> validate_length(:street, min: 3, max: 100)
|> validate_length(:city, min: 2, max: 50)
|> validate_format(:zip, ~r/^\d{5}$/, message: "must be 5 digits")
end
end
defmodule Profile do
use Ecto.Schema
import Ecto.Changeset
@derive LiveVue.Encoder
@primary_key false
embedded_schema do
field :name, :string
field :email, :string
embeds_one :address, Address, on_replace: :update
end
def changeset(profile, attrs) do
profile
|> cast(attrs, [:name, :email])
|> validate_required([:name, :email])
|> validate_length(:name, min: 2, max: 50)
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must be a valid email")
|> cast_embed(:address, with: &Address.changeset/2)
end
end
def render(assigns) do
~H"""
<.vue
form={@form}
submitted={@submitted}
v-component="NestedForm"
v-socket={@socket}
/>
"""
end
def mount(_params, _session, socket) do
profile = %Profile{address: %Address{}}
changeset = Profile.changeset(profile, %{})
{:ok, assign(socket, form: to_form(changeset, as: :profile), submitted: nil)}
end
def handle_event("validate", %{"profile" => params}, socket) do
profile = %Profile{address: %Address{}}
changeset =
profile
|> Profile.changeset(params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, form: to_form(changeset, as: :profile))}
end
def handle_event("submit", %{"profile" => params}, socket) do
profile = %Profile{address: %Address{}}
changeset =
profile
|> Profile.changeset(params)
|> Map.put(:action, :insert)
if changeset.valid? do
data = Ecto.Changeset.apply_changes(changeset)
{:reply, %{reset: true},
socket
|> assign(submitted: data)
|> assign(
form: to_form(Profile.changeset(%Profile{address: %Address{}}, %{}), as: :profile)
)}
else
{:noreply, assign(socket, form: to_form(changeset, as: :profile))}
end
end
end
How it works
1 Define an embedded schema or relation
Use embeds_one
or has_one/belongs_to to define nested objects. Each gets its own changeset and validation.
defmodule Address do
use Ecto.Schema
@derive LiveVue.Encoder
embedded_schema do
field :street, :string
field :city, :string
field :zip, :string
end
end
embeds_one :address, Address, on_replace: :update
2 Use cast_embed for validation
Call
cast_embed(:address)
to automatically validate nested fields. Errors are mapped to the correct nested paths.
def changeset(profile, attrs) do
profile
|> cast(attrs, [:name, :email])
|> validate_required([:name, :email])
|> cast_embed(:address, with: &Address.changeset/2)
end
3 Access nested fields with dot notation or chaining
Use
form.field("address.city")
or chain with .field().
Both have full TypeScript support for type-safe paths. Use `field[index]` to access nested array fields, eg `form.field("tags[0].name")`.
// Dot notation
const cityField = form.field("address.city")
// Or chain .field() calls
const addressField = form.field("address")
const cityField = addressField.field("city")
4 Bind inputs exactly like flat fields
Once you have a field reference, use it exactly like any other field.
The inputAttrs, errorMessage, and
isTouched
work identically.
<input v-bind="cityField.inputAttrs.value" type="text" />
<div v-if="cityField.errorMessage.value">
{{ cityField.errorMessage.value }}
</div>