LiveVue 1.0
Examples Dynamic Arrays

Dynamic Arrays

Use fieldArray() to manage dynamic lists with add, remove, and per-item validation.

What this example shows

1
fieldArray()
form.fieldArray("tags")
2
Add & Remove
tagsArray.add(), .remove(i)
3
Per-Item Errors
tags[0], tags[1] validation
array_form_live.ex
defmodule MyAppWeb.ArrayFormLive do
  use MyAppWeb, :live_view

  import Ecto.Changeset

  defmodule Tag do
    use Ecto.Schema
    import Ecto.Changeset

    @derive LiveVue.Encoder
    @primary_key false
    embedded_schema do
      field :name, :string
    end

    def changeset(tag, attrs) do
      tag
      |> cast(attrs, [:name], empty_values: [])
      |> validate_required([:name])
      |> validate_length(:name, min: 2, max: 30)
    end
  end

  defmodule Post do
    use Ecto.Schema
    import Ecto.Changeset

    @derive LiveVue.Encoder
    @primary_key false
    embedded_schema do
      field :title, :string
      embeds_many :tags, Tag, on_replace: :delete
    end

    def changeset(post, attrs) do
      post
      |> cast(attrs, [:title])
      |> validate_required([:title])
      |> validate_length(:title, min: 2, max: 100)
      |> cast_embed(:tags, with: &Tag.changeset/2)
    end
  end

  def render(assigns) do
    ~H"""
    <.vue
      form={@form}
      submitted={@submitted}
      v-component="ArrayForm"
      v-socket={@socket}
    />
    """
  end

  def mount(_params, _session, socket) do
    changeset = Post.changeset(%Post{}, %{})
    {:ok, assign(socket, form: to_form(changeset, as: :post), submitted: nil)}
  end

  def handle_event("validate", %{"post" => params}, socket) do
    changeset =
      %Post{}
      |> Post.changeset(params)
      |> Map.put(:action, :validate)

    {:noreply, assign(socket, form: to_form(changeset, as: :post))}
  end

  def handle_event("submit", %{"post" => params}, socket) do
    changeset =
      %Post{}
      |> Post.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(Post.changeset(%Post{}, %{}), as: :post))}
    else
      {:noreply, assign(socket, form: to_form(changeset, as: :post))}
    end
  end
end

How it works

1 Define an embedded schema or relation

Use embeds_many or has_many to define array items. Each item gets its own changeset and validation.

defmodule Tag do
  use Ecto.Schema
  @derive LiveVue.Encoder
  embedded_schema do
    field :name, :string
  end
end

embeds_many :tags, Tag, on_replace: :delete

2 Get an array field reference

Use form.fieldArray("tags") to get a reactive array with add, remove, and iteration capabilities. Nested access is supported.

const tagsArray = form.fieldArray("tags")

// Add a new tag object
tagsArray.add({ name: '' })

// Remove tag at index
tagsArray.remove(index)

3 Iterate and access nested fields

Loop over tagsArray.fields.value and use tagField.field('name') to access each item's properties with full error support.

<div v-for="(tagField, index) in tagsArray.fields.value" :key="index">
  <input v-bind="tagField.field('name').inputAttrs.value" />
  <div v-if="tagField.field('name').errorMessage.value">
    {{  tagField.field('name').errorMessage.value }}
  </div>
  <button @click="tagsArray.remove(index)">Remove</button>
</div>

4 Use cast_embed for validation

Call cast_embed(:tags) to automatically validate each item using its own changeset. Errors are mapped per-item.

def changeset(post, attrs) do
  post
  |> cast(attrs, [:title])
  |> validate_required([:title])
  |> cast_embed(:tags, with: &Tag.changeset/2)
end
Next up: File uploads with useLiveUpload()
View example →