Skip to content

Arrays

vue-form renders any T[] field through the AsArray Tier-2 default. Add and remove are wired automatically, length constraints flow from @expect.minLength / @expect.maxLength, and the item-noun for the "Add" button is configurable.

Patterns at a glance

text
T[]   →  AsArray
T[]?  →  AsArray with "enable this section" affordance when undefined

Four shapes show up in practice:

  1. Optional scalar arraystags?: string[]. Collapsed to an Add placeholder until the user starts.
  2. Required scalar arraysheaders: string[]. Starts with zero items; submit blocks on @expect.minLength 1.
  3. Object arrayscontacts?: { ... }[]. Each item renders as a card with its own Remove button.
  4. Union-item arraysevents: EventA | EventB[]. Adding picks a variant; switching wipes the item data.

Plus the composition pattern: nested arraysteam?: { name, members?: string[] }[].

The canonical example

A Team.as schema that covers every array shape in one form.

atscript
@meta.label 'Array Showcase'
@meta.description 'Every array variant in one form.'
@ui.form.submit.text 'Save'
export interface ArrayShowcaseForm {
    // Optional array of primitives — collapsed to the Add placeholder
    // when empty.
    @meta.label 'Tags'
    @ui.form.label.singular 'tag'
    tags?: string[]

    // Required array of primitives — opens with zero items and a
    // footer Add button.
    @meta.label 'HTTP headers'
    @ui.form.label.singular 'header'
    @expect.minLength 1, 'At least one header is required'
    headers: string[]

    // Optional array of objects — each item is a card.
    @meta.label 'Contacts'
    @ui.form.label.singular 'contact'
    contacts?: {
        @meta.label 'First name'
        @meta.required 'First name is required'
        @ui.form.grid.colSpan '6'
        firstName: string

        @meta.label 'Last name'
        @ui.form.grid.colSpan '6'
        lastName?: string

        @meta.label 'Email'
        @meta.required 'Email is required'
        email: string.email
    }[]

    // Required array of objects — at least one entry.
    @meta.label 'Phone numbers'
    @ui.form.label.singular 'phone'
    @expect.minLength 1, 'At least one phone is required'
    phones: {
        @meta.label 'Label'
        @meta.required 'Label is required'
        @ui.form.grid.colSpan '4'
        label: string

        @meta.label 'Number'
        @meta.required 'Number is required'
        @ui.form.grid.colSpan '8'
        number: string
    }[]

    // Nested array.
    @meta.label 'Team'
    @ui.form.label.singular 'team'
    team?: {
        @meta.label 'Team name'
        @meta.required 'Team name is required'
        name: string

        @meta.label 'Members'
        @ui.form.label.singular 'member'
        members?: string[]
    }[]
}

Mount the form the usual way and every array gets +/− buttons, count badges, and per-item validation for free:

vue
<script setup lang="ts">
import { AsForm, createDefaultTypes, createAsFormDef } from "@atscript/vue-form";
import { ArrayShowcaseForm } from "./ArrayShowcase.as";

const types = createDefaultTypes();
const { def, formData } = createAsFormDef(ArrayShowcaseForm);
</script>

<template>
  <AsForm :def="def" :form-data="formData" :types="types" @submit="onSave" />
</template>

Add / Remove constraints

canAdd and canRemove are derived from the length constraints (packages/vue-form/src/composables/use-as-array.ts:92-98):

ts
const canAdd = !disabled && length < maxLength;
const canRemove = !disabled && length > minLength;
AnnotationEffect
@expect.minLength N, 'msg'Add starts enabled; Remove hides once length === N
@expect.maxLength N, 'msg'Add hides once length === N
@ui.form.disabled on the array propBoth Add and Remove disable

Validation runs at submit (and live, depending on firstValidation) — exceeding maxLength is gated by the UI, but minLength is enforced by both the UI (Remove disabled) and the submit-time validator.

The "Add ___" button text

@ui.form.label.singular 'tag' controls the noun used in the Add button: "Add tag", "Add tag", "Add tag". Without it the default is "item" — fine for prototypes, not for real UI.

Falls back to the item's prop name if not set on the array prop — useful for tuples and inline arrays. See packages/vue-form/src/components/as-field.vue:308-316.

Item identity

Each item gets a stable string key generated by useAsArray (as-item-0, as-item-1, …). The key persists across remounts as long as the item doesn't get re-added; removing item 2 from [a, b, c] leaves a and c with their original keys.

This is what makes per-item validation behave correctly — a fresh-just- added item stays fresh; an existing item that gets edited promotes out of freshness, no matter what its current array index is.

The key is exposed on useAsArray()'s itemKeys array if you write a custom array renderer.

Nested arrays

Nested arrays just work — AsArray recurses through AsFieldAsArray. Each level provides its own PATH_PREFIX_KEY so child fields compute absolute paths automatically (team.0.members.2). Required/optional toggles cascade naturally: an optional outer array is collapsed until enabled; a required inner array under it starts with the inner constraint.

Union-item arrays

events: EventA | EventB[] renders the Add button as a multi-action menu (one Add per variant). Switching variants on an existing item wipes the item's data — see Unions for the underlying behaviour and how to preserve user input across switches.

Custom array renderer

If you want a fully custom array UI — a sortable list, a chip input, an SAP-like detail-master — call useAsArray(field) from a Tier-2 swap component:

vue
<script setup lang="ts">
import { useAsArray, AsField, AsFieldShell, type TAsComponentProps } from "@atscript/vue-form";
import type { FormArrayFieldDef } from "@atscript/ui";

const props = defineProps<TAsComponentProps>();
const {
  arrayValue,
  itemKeys,
  isUnion,
  unionVariants,
  isEmpty,
  getItemField,
  addItem,
  removeItem,
  canAdd,
  canRemove,
} = useAsArray(props.field as FormArrayFieldDef);
</script>

<template>
  <AsFieldShell v-bind="$props">
    <template #default>
      <div v-for="(_, idx) in arrayValue" :key="itemKeys[idx]">
        <AsField :field="getItemField(idx)" />
        <button v-if="canRemove" @click="removeItem(idx)">−</button>
      </div>
      <button v-if="canAdd" @click="addItem()">Add</button>
    </template>
  </AsFieldShell>
</template>

Register it via the :components map and opt-in per field with @ui.form.component 'sortable-array'.

Phantom helpers

@ui.array.add.label 'X' (alias for @ui.form.label.singular) is still accepted for backwards compatibility — both produce the same button text. Prefer @ui.form.label.singular.

Next steps

Released under the MIT License.