Guide

Collections and wizards

Build repeaters and block schemas, preserve stable client identities, and guide users through fieldsets.

Collections keep nested data and validation paths aligned between Laravel and Vue. Wizards layer navigation over visible fieldsets. Both remain renderless, so your application controls every list, button, step indicator, and transition.

Repeaters

Use a repeater for rows that share one schema.

use Inertify\Form\Fields\Repeater;
use Inertify\Form\Fields\Textarea;
use Inertify\Form\Fields\TextInput;

Repeater::make('projects', 'Projects')
    ->schema([
        TextInput::make('title')->required(),
        Textarea::make('summary')->maxLength(500),
    ])
    ->default([['title' => '', 'summary' => '']])
    ->defaultItem(['title' => '', 'summary' => ''])
    ->minItems(1)
    ->maxItems(10)
    ->reorderable();

Nested validation rules are generated for the submitted row indexes. Inside row conditions, bare paths resolve against the row first; use a $. prefix to refer to root data.

FormCollection exposes stable keys and collection operations without rendering a wrapper:

resources/js/Pages/Projects/Edit.vue
<script setup lang="ts">
import { FormCollection } from '@inertify/form-vue'
import { FormField } from '@/components/form'
</script>

<template>
  <FormCollection
    name="projects"
    v-slot="{ items, keys, canAppend, append, remove, move }"
  >
    <section v-for="(_, index) in items" :key="keys[index]">
      <FormField :name="`projects.${index}.title`" />
      <FormField :name="`projects.${index}.summary`" />

      <button type="button" :disabled="index === 0" @click="move(index, index - 1)">
        Move up
      </button>
      <button type="button" @click="remove(index)">Remove</button>
    </section>

    <button type="button" :disabled="!canAppend" @click="append()">
      Add project
    </button>
  </FormCollection>
</template>

The equivalent useFormCollection(path, form?) API provides append, prepend, insert, update, remove, move, swap, duplicate, and clear. canAppend respects maxItems; minimum counts are still enforced by server validation.

Blocks

Use blocks when each row can have a different schema.

use Inertify\Form\Fields\Blocks;
use Inertify\Form\Fields\BlockSet;
use Inertify\Form\Fields\Textarea;
use Inertify\Form\Fields\TextInput;

Blocks::make('content')->sets([
    BlockSet::make('hero', 'Hero')
        ->schema([
            TextInput::make('headline')->required(),
            Textarea::make('intro'),
        ])
        ->default(['headline' => '', 'intro' => ''])
        ->maxItems(1),
    BlockSet::make('quote', 'Quote')->schema([
        Textarea::make('quote')->required(),
        TextInput::make('attribution'),
    ]),
])
    ->minBlocks(1)
    ->maxBlocks(12)
    ->reorderable();

Block rows use { type, data }. Call appendBlock('hero') on a collection controller—or use the same callback from FormCollection—to insert the selected set with its configured defaults. Per-set and overall maximums are checked before appending.

Configure a wizard

A wizard step corresponds to a visible fieldset. Configure its labels and guards on the form:

use Inertify\Form\WizardConfig;

public function wizard(): WizardConfig
{
    return WizardConfig::make()
        ->step('Account', 'Identity and contact details')
        ->step('Experience', 'Skills and projects')
        ->step('About', 'Biography and avatar')
        ->validateOnStep()
        ->labels(next: 'Continue', prev: 'Back', submit: 'Save profile');
}

With no explicit step() calls, every visible fieldset becomes a step using its legend and description. allowSkip() permits jumping over intermediate steps. Hidden or empty fieldsets do not become server-side wizard steps.

Render navigation with FormWizard and the same registered field components:

<Form :form="form" v-slot="{ form: formApi, processing }">
  <FormWizard v-slot="{ current, isFirst, isLast, previous, next, labels }">
    <h2 v-if="current">{{ current.label }}</h2>
    <p v-if="current?.description">{{ current.description }}</p>

    <FormFields
      v-if="current"
      :form="formApi"
      :fields="current.fields"
    />

    <button v-if="!isFirst" type="button" @click="previous">
      {{ labels.previous }}
    </button>
    <button v-if="!isLast" type="button" @click="next">
      {{ labels.next }}
    </button>
    <button v-else type="submit" :disabled="processing">
      {{ labels.submit }}
    </button>
  </FormWizard>
</Form>

When validateOnStep() is active, next() validates the current step's precognitive() fields and refuses to advance while they have errors. useFormWizard() also exposes steps, currentIndex, completed, goTo(), validateCurrent(), and reset(). Wizard is a convenience component that creates and provides both the form and wizard engines, and—like Form—renders the <form> element around its slot content.

Copyright © 2026 Inertify · Released under the MIT License