Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Inertia Forms Laravel Package

erag/inertia-forms

erag/inertia-forms simplifies form handling in Laravel Inertia apps, pairing server-side validation with a lightweight frontend API for submitting, resetting, and displaying errors—cutting boilerplate and keeping forms consistent.

View on GitHub
Deep Wiki
Context7

Getting Started

What you're adopting. One PHP class owns a form end-to-end — fields, layout, validation rules, submit route and HTTP method — and every frontend renders it through a single <Form> component. Your Vue/React/Svelte page stops describing inputs and starts mounting a server-authored form description.

Install. You need the PHP package plus exactly one frontend adapter for your stack:

composer require erag/inertia-forms
npm i @erag/inertia-forms-vue      # or -react / -svelte

The frontend adapter is the real commitment — it's what renders all 22+ field types, so it ships the UI. Pin its version and treat upgrades as a review item (see Gotchas).

Look here first, in this order:

  1. https://erag.in/laravel-inertia-forms/llms.txt — the LLM-friendly index. Point your agent at it; it's the fastest way to get an assistant writing correct field chains instead of guessing method names.
  2. The field table (reproduced in the skill) — skim it once so you know DatePicker supports range(), Combobox supports searchUsing(), and Composer/OtpInput exist. You will not remember 22 field types; you only need to remember what's possible.
  3. app/Forms/ after your first make:form — the generated stub is the canonical shape.

First use case: the profile edit form. Smallest thing that exercises binding, validation, and submission.

php artisan make:form EditProfileForm
class EditProfileForm extends Form
{
    protected ?string $actionRoute = 'profile.update';
    protected ?string $method = 'put';

    public function fields(): array
    {
        return [
            TextInput::make('name')->required()->autofocus(),
            TextInput::make('email')->email()->required()->autocomplete('email'),
            Textarea::make('bio')->rows(4)->maxLength(500)->showCharacterCount(),
            Submit::make('Save changes')->primary(),
        ];
    }
}

Controller:

public function edit(Request $request)
{
    return Inertia::render('Profile/Edit', [
        'form' => EditProfileForm::make()->bind($request->user()),
    ]);
}

public function update(#[Validate] EditProfileForm $form)
{
    auth()->user()->update($form->validated());

    return back()->with('success', 'Profile saved.');
}

Page:

<script setup lang="ts">
import { Form } from '@erag/inertia-forms-vue'
defineProps<{ form: object }>()
</script>

<template>
  <Form :form="form" />
</template>

That's the whole loop. Once this works, the same pattern scales to create/edit/settings/checkout without ever writing a second form template.


Implementation Patterns

The form class is the single source of truth

The highest-leverage habit is refusing to split a form across a FormRequest, a Blade/Vue template, and a controller. fields() carries the rules; #[Validate] resolves them into a validator; validated() returns the safe payload. One class, one diff to review when a form changes.

StoreProjectForm::make()                    // create
StoreProjectForm::make()->bind($project)    // edit — values come from the model

Layout is data, not markup

Fieldset::make()->columns(2)->icon() groups fields and sets the grid; columnSpan() sizes individual fields inside it. Because it's PHP, you can build fieldsets from a loop — useful for repeating sections driven by config.

Wizard mode for multi-step

Set protected bool $wizard = true; and each Fieldset becomes a step. This is the pattern that justifies the package most: a 4-step checkout with per-step fieldsets and a single submit endpoint is otherwise a lot of hand-written step state on the frontend.

class CheckoutForm extends Form
{
    protected ?string $actionRoute = 'checkout.store';
    protected bool $wizard = true;

    public function fields(): array
    {
        return [
            Fieldset::make('Contact')->icon('user')->columns(2)->fields([
                TextInput::make('name')->required(),
                TextInput::make('email')->email()->required(),
            ]),
            Fieldset::make('Delivery')->icon('truck')->fields([
                Radio::make('method')
                    ->options(['ship' => 'Ship it', 'pickup' => 'Pick up'])
                    ->buttons()->required(),
                TextInput::make('address')
                    ->required()
                    ->visibleWhen('method', 'ship')
                    ->clearWhenHidden(),
            ]),
            Submit::make('Pay now')->icon('creditCard'),
        ];
    }
}

Note clearWhenHidden() — pair it with every conditional field that has required() or you'll fight phantom validation.

Conditional logic is declarative

visibleWhen() / hiddenWhen() drive the UI; authorize() / authorizedWhen() / authorizedUnless() gate the field server-side and keep it out of validated(). Use both when the distinction is "irrelevant" vs "not permitted."

Multiple submit intents

Several Submit buttons, each with intent('draft') or intent('publish'), send the intent alongside the payload. Read it with $form->validated('intent') and branch:

public function store(#[Validate] PostForm $form)
{
    $post = Post::create($form->validated());

    if ($form->validated('intent') === 'publish') {
        $post->publish();
    }

    return to_route('posts.show', $post);
}

This removes the usual "hidden input + $request->input('action')" hack entirely.

Server-backed selects

Combobox::make('user_id')->searchable()->searchUsing(...)->selectedOptionsUsing(...) keeps search server-side, so you never ship 20k options to the browser. Reach for this the moment an option list is not static.

Repeater and Blocks for structured payloads

Repeater::make('items')->fields([...])->minItems(1)->maxItems(10) covers invoice lines, team invites, and address lists. Blocks gives you a block-picker (hero/text/gallery) with per-block fields — the standard shape of a CMS page builder. The validated payload arrives as nested arrays; cast or map it in one place rather than sprinkling foreach in the controller.

Custom fields are a two-part extension

A PHP class extending Field with component() returning a name, plus a frontend component registered on the form element:

<Form :form="form" :components="{ Bolt: BoltField }" />

Keep that components object stable (module-scope constant, not an inline literal) to avoid remounting on every render.

Testing

Because fields() is plain PHP, form classes are unit-testable: assert validated() contains/excludes keys under various authorization and condition states, without hitting the browser. Then one HTTP test through the controller with #[Validate] verifies wiring.


Gotchas and Tips

visibleWhen() is not security. Use authorizedWhen() / authorizedUnless() for anything a user must not be able to submit. A hidden field's client-side absence means nothing — the rules were already shipped to the page.

Don't duplicate rules in a FormRequest. The fields already produce validation rules. Duplicating them means two places to update and a guaranteed drift bug. Add extra rules with rules() / rule() on the field.

clearWhenHidden() is not optional in practice. A required field hidden by a condition will still validate against a stale value unless cleared. Symptom: "the field isn't even on screen" validation errors.

$method for non-POST submits. protected ?string $method = 'put'; (or fluent ->put()) must be set or updates hit the wrong verb. This is the single most common first-day bug and produces a confusing 405 rather than a validation error.

Display-only fields are invisible to validated(). Heading, Text, Html, Separator, Callout are layout only. Don't try to persist them; conversely, don't panic when they're missing from the array.

Hidden fields still carry values. Hidden::make('token') pulls from default() or the bound model. On an edit form bound to a model, verify you aren't shipping a secret into the page payload — bind selectively or null the default.

validated() returns only what's allowed. Hidden, unauthorized, and cleared fields are excluded. Never $form->all() into a model unless you've consciously decided mass assignment is safe.

Debugging a form that renders wrong. Check the serialized form prop in the Inertia network response or Inertia devtools before touching the frontend — the PHP field graph is the truth, and 90% of "the component is broken" is a field with a null label, bad options(), or a route that doesn't exist. Then verify the PHP and frontend package versions match; adapters and the composer package are versioned separately and mismatches surface as unknown type values or blank inputs.

Icons. 200+ names ship built in (user, rocket, shoppingCart / shopping-cart — both casings work). Register your own with Icon::register('bolt', '<path d="…"/>') or via config('inertia-forms.icons'). Register once in a service provider, not inline in a form class.

Wizard + validation per step. Step-gating behaviour (does it validate only the current fieldset, or everything?) is the thing most worth verifying against your version's docs before you build a five-step flow. If it validates wholesale, users will see errors for steps they haven't reached — design your required fields accordingly or split into separate forms per step.

File uploads and multipart. FileUpload with multiple() / image() / maxSize() describes the field, but the transport is Inertia's. Confirm on your adapter version that uploads are sent as FormData (not JSON) before shipping, and test large/multi-file cases explicitly.

Maturity risk is the real cost. 24 stars, zero dependents, and a single primary release cadence means: no ecosystem of Stack Overflow answers, breaking changes arrive without a crowd noticing, and the frontend adapters are yours to debug. Mitigations that actually help:

  • Pin exact versions in package.json (not ^) and commit the lockfile.
  • Keep form classes thin and self-contained so the migration to hand-written Vue/React inputs is a per-form rewrite, not a page rewrite. The PHP side is cheap to replace; resist putting business logic inside fields() closures.
  • Write the HTTP-level feature test for each critical form. It survives a frontend swap and catches adapter regressions on upgrade.
  • Avoid Composer/OtpInput/Blocks on day one unless you genuinely need them — start with TextInput/Textarea/Combobox/Submit and grow. Adopting exotic field types early maximizes your exposure to the parts of the library with the fewest users.

Where the leverage actually is. This package pays off on many small-to-medium forms with varied field types — settings pages, onboarding, admin CRUD — where the alternative is 15 nearly-identical Vue components. It pays off less on one giant bespoke form with heavy custom interaction, where you'll be writing custom field components anyway and the abstraction becomes a layer between you and the DOM.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console