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
ArrayForm.vue
<script setup lang="ts">
import { useLiveForm, type Form } from "live_vue"

type Tag = {
  name: string
}

type PostForm = {
  title: string
  tags: Tag[]
}

const props = defineProps<{ form: Form<PostForm>; submitted: PostForm | null }>()

const form = useLiveForm(() => props.form, {
  changeEvent: "validate",
  submitEvent: "submit",
  debounceInMiliseconds: 300
})

const titleField = form.field("title")
const tagsArray = form.fieldArray("tags")
</script>

<template>
  <div class="card bg-base-200 p-6 space-y-6">
    <div class="flex flex-col gap-6">
      <label class="form-control w-full">
        <div class="label pb-2"><span class="label-text font-medium">Title</span></div>
        <input
          v-bind="titleField.inputAttrs.value"
          type="text"
          placeholder="Enter a title"
          :class="[
            'input input-bordered w-full',
            titleField.isTouched.value && titleField.errorMessage.value && 'input-error'
          ]"
        />
        <div v-if="titleField.isTouched.value && titleField.errorMessage.value" class="label">
          <span class="label-text-alt text-error">{{ titleField.errorMessage.value }}</span>
        </div>
      </label>

      <div class="pt-4 border-t border-base-300">
        <div class="flex items-center justify-between mb-4">
          <div class="text-sm font-medium">Tags</div>
          <button
            type="button"
            class="btn btn-xs btn-primary btn-outline"
            @click="tagsArray.add({ name: '' })"
          >
            + Add Tag
          </button>
        </div>

        <div class="space-y-3">
          <div
            v-for="(tagField, index) in tagsArray.fields.value"
            :key="index"
            class="flex gap-2 items-start"
          >
            <label class="form-control flex-1">
              <input
                v-bind="tagField.field('name').inputAttrs.value"
                type="text"
                :placeholder="`Tag ${index + 1}`"
                :class="[
                  'input input-bordered w-full',
                  tagField.field('name').isTouched.value && tagField.field('name').errorMessage.value && 'input-error'
                ]"
              />
              <div
                v-if="tagField.field('name').isTouched.value && tagField.field('name').errorMessage.value"
                class="label"
              >
                <span class="label-text-alt text-error">{{ tagField.field('name').errorMessage.value }}</span>
              </div>
            </label>
            <button
              type="button"
              class="btn btn-square btn-ghost btn-sm shrink-0 mt-0.5"
              @click="tagsArray.remove(index)"
              title="Remove tag"
            >
              <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
                <path
                  fill-rule="evenodd"
                  d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
                  clip-rule="evenodd"
                />
              </svg>
            </button>
          </div>
          <div
            v-if="tagsArray.fields.value.length === 0"
            class="text-sm text-neutral/50 py-8 text-center border border-dashed border-base-300 rounded-lg"
          >
            No tags yet. Click "Add Tag" to start.
          </div>
        </div>
      </div>
    </div>

    <div class="flex items-center justify-between pt-2">
      <div class="flex items-center gap-3">
        <button
          type="button"
          :disabled="!form.isValid.value"
          class="btn btn-primary"
          @click="form.submit()"
        >
          Submit
        </button>
        <button
          type="button"
          class="btn btn-ghost"
          @click="form.reset()"
        >
          Reset
        </button>
      </div>
      <div class="flex items-center gap-4 text-xs">
        <span :class="form.isDirty.value ? 'text-primary' : 'text-neutral/50'">
          {{ form.isDirty.value ? "Modified" : "Unchanged" }}
        </span>
        <span :class="form.isValid.value ? 'text-success' : 'text-error'">
          {{ form.isValid.value ? "Valid" : "Invalid" }}
        </span>
      </div>
    </div>

    <div v-if="props.submitted" class="mt-6 p-4 bg-success/10 border border-success/20 rounded-lg">
      <div class="text-sm font-medium text-success mb-2">Submitted Data:</div>
      <pre class="text-xs overflow-auto">{{ JSON.stringify(props.submitted, null, 2) }}</pre>
    </div>
  </div>
</template>

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 →