Boson

Form

Form with CSRF and method spoofing. Add fetch for JS-powered submission with in-page data updates, validation errors, and response handling — or let Turbo handle navigation automatically.


Basic usage

<x-boson::form action="/projects" method="POST">
    <x-boson::input name="name" label="Project name" />
    <x-boson::error name="name" />
    <x-boson::button type="submit">Create</x-boson::button>
</x-boson::form>

With Turbo

{{-- When Turbo is installed, forms use Turbo Drive by default --}}
<x-boson::form action="/login" method="POST">
    <x-boson::input name="email" label="Email" type="email" />
    <x-boson::input name="password" label="Password" type="password" />
    <x-boson::button type="submit">Log in</x-boson::button>
</x-boson::form>

{{-- To disable Turbo on a specific form --}}
<x-boson::form :turbo="false" action="/login" method="POST">
    ...
</x-boson::form>

Turbo attributes

{{-- Confirmation dialog before Turbo submits --}}
<x-boson::form action="/teams/1" method="DELETE" turbo:confirm="Delete this team?">
    <x-boson::button type="submit" variant="danger" turbo:submits-with="Deleting...">
        Delete Team
    </x-boson::button>
</x-boson::form>

{{-- Target a Turbo Frame --}}
<x-boson::form action="/search" method="GET" turbo:frame="results">
    <x-boson::input name="q" placeholder="Search..." />
</x-boson::form>

{{-- Request a Turbo Stream response --}}
<x-boson::form action="/comments" method="POST" turbo:stream>
    <x-boson::textarea name="body" label="Comment" />
    <x-boson::button type="submit">Post</x-boson::button>
</x-boson::form>

Fetch with in-page updates

Team name

Acme Inc.

{{-- Display area — updates in-place on success --}}
<h2 data-field="name">Acme Inc.</h2>

{{-- Add fetch for JS-powered submission --}}
<x-boson::form fetch action="/teams/1" method="PUT">
    <x-boson::input name="name" label="Team name" :value="$team->name" />
    <x-boson::error name="name" />
    <x-boson::button type="submit">Save</x-boson::button>
</x-boson::form>
{{-- Controller --}}
public function update(Request $request, Team $team)
{
    $validated = $request->validate([
        'name' => ['required', 'string', 'max:255'],
    ]);

    $team->update($validated);

    // Return JSON with a `data` key → triggers in-page update
    return response()->json([
        'data' => $team,
    ]);
}

Nested data (dot-notation)

Name: Jane Doe

Email: jane@example.com

Role: Editor

{{-- Display area with dot-notation fields --}}
<p>Name: <span data-field="user.name">Jane Doe</span></p>
<p>Email: <span data-field="user.email">jane@example.com</span></p>
<p>Role: <span data-field="role">Editor</span></p>

{{-- Add fetch for JS-powered submission --}}
<x-boson::form fetch action="/users/1" method="PUT">
    <x-boson::input name="name" label="Name" :value="$user->name" />
    <x-boson::input name="email" label="Email" type="email" :value="$user->email" />
    <x-boson::input name="role" label="Role" :value="$user->role" />
    <x-boson::button type="submit">Update profile</x-boson::button>
</x-boson::form>
{{-- Controller returning nested data --}}
public function update(Request $request, User $user)
{
    $user->update($request->validated());

    // Nested objects flatten to dot-notation:
    //   { user: { name: "Jane" } } → matches [data-field="user.name"]
    return response()->json([
        'data' => [
            'user' => [
                'name'  => $user->name,
                'email' => $user->email,
            ],
            'role' => $user->role,
        ],
    ]);
}

Validation errors

{{-- Place an error component after each input. Errors work with fetch forms. --}}
<x-boson::form fetch action="/users" method="POST">
    <x-boson::input name="name" label="Name" />
    <x-boson::error name="name" />

    <x-boson::input name="email" label="Email" type="email" />
    <x-boson::error name="email" />

    <x-boson::button type="submit">Create user</x-boson::button>
</x-boson::form>

{{-- When the controller returns 422, errors auto-populate.
     Laravel's default validation already returns:
     { errors: { email: ["The email is required."] } } --}}

Multiple input types

Project: Untitled

Description: No description yet.

Public: No

{{-- Display area --}}
<p>Project: <span data-field="project_name">Untitled</span></p>
<p>Description: <span data-field="description">No description yet.</span></p>
<p>Public: <span data-field="is_public">No</span></p>

{{-- Form with mixed input types --}}
<x-boson::form fetch action="/projects" method="POST">
    <x-boson::input name="project_name" label="Project name" />
    <x-boson::error name="project_name" />

    <x-boson::textarea name="description" label="Description" />
    <x-boson::error name="description" />

    <x-boson::checkbox name="is_public" label="Make this project public" />

    <x-boson::button type="submit">Create project</x-boson::button>
</x-boson::form>
{{-- Controller --}}
public function store(Request $request)
{
    $validated = $request->validate([
        'project_name' => ['required', 'string', 'max:255'],
        'description'  => ['nullable', 'string'],
        'is_public'    => ['boolean'],
    ]);

    $project = Project::create($validated);

    return response()->json([
        'data' => [
            'project_name' => $project->name,
            'description'  => $project->description ?: 'No description yet.',
            'is_public'    => $project->is_public ? 'Yes' : 'No',
        ],
    ]);
}

Redirect response

{{-- When using fetch, a standard redirect still works --}}
<x-boson::form fetch action="/projects" method="POST">
    ...
</x-boson::form>

{{-- Controller returning a redirect --}}
public function store(Request $request)
{
    $project = Project::create($request->validated());

    // Standard redirect → the page navigates normally
    return redirect("/projects/{$project->id}");
}

Inside a modal

{{-- On success the form resets and the parent modal closes automatically --}}
<x-boson::modal title="Create team">
    <x-boson::form fetch action="/teams" method="POST">
        <x-boson::input name="name" label="Team name" />
        <x-boson::error name="name" />
        <x-boson::button type="submit">Create</x-boson::button>
    </x-boson::form>
</x-boson::modal>

Behavior modifiers

{{-- Keep form values after success (skip reset) --}}
<x-boson::form fetch action="/settings" method="PUT" data-no-reset-on-success>
    <x-boson::input name="timezone" label="Timezone" />
    <x-boson::button type="submit">Save</x-boson::button>
</x-boson::form>

{{-- Keep modal open after success --}}
<x-boson::modal title="Add tags">
    <x-boson::form fetch action="/tags" method="POST" data-no-close-on-success>
        <x-boson::input name="tag" label="Tag" />
        <x-boson::button type="submit">Add</x-boson::button>
    </x-boson::form>
</x-boson::modal>

Full fetch example

Acme Corp

Building the future of widgets.

Owner: Jane Smith

{{-- Display area — updated in-place on form success --}}
<h3 data-field="name">{{ $team->name }}</h3>
<p data-field="description">{{ $team->description }}</p>
<p>Owner: <span data-field="owner.name">{{ $team->owner->name }}</span></p>

{{-- Edit form with fetch --}}
<x-boson::form fetch action="/teams/{{ $team->id }}" method="PUT">
    <x-boson::input name="name" label="Name" :value="$team->name" />
    <x-boson::error name="name" />

    <x-boson::textarea name="description" label="Description">
        {{ $team->description }}
    </x-boson::textarea>
    <x-boson::error name="description" />

    <x-boson::button type="submit">Update team</x-boson::button>
</x-boson::form>
{{-- Controller: app/Http/Controllers/TeamController.php --}}

public function update(Request $request, Team $team)
{
    $validated = $request->validate([
        'name'        => ['required', 'string', 'max:255'],
        'description' => ['nullable', 'string'],
    ]);

    $team->update($validated);

    // Return `data` key → in-page update, no redirect
    // Nested relations are flattened with dot-notation
    return response()->json([
        'data' => [
            'name'        => $team->name,
            'description' => $team->description,
            'owner'       => [
                'name' => $team->owner->name,
            ],
        ],
    ]);
}

JavaScript events

{{-- Listen to form lifecycle events (fetch forms only) --}}
<script>
const form = document.querySelector('form');

form.addEventListener('boson:submitting', (e) => {
    // Fires before fetch — call e.preventDefault() to cancel
});

form.addEventListener('boson:success', (e) => {
    // Fires after a 2xx response
    console.log(e.detail); // Response data
});

form.addEventListener('boson:error', (e) => {
    // Fires after 4xx/5xx or network error
    console.log(e.detail); // { errors, status }
});

form.addEventListener('boson:submitted', (e) => {
    // Fires after any response (success or error)
});
</script>

Props

Prop Type Default Description
action string Form action URL
method string POST HTTP method (GET, POST, PUT, PATCH, DELETE). PUT/PATCH/DELETE are automatically spoofed.
fetch bool false Submit via JavaScript fetch with JSON response handling, in-page updates, and validation errors.
turbo bool true Allow Turbo Drive to handle form submission. Set :turbo="false" to use standard browser submission.
turbo:* string Turbo data attributes (e.g. turbo:confirm, turbo:frame, turbo:stream). Maps to data-turbo-*.

Data attributes

Prop Type Default Description
data-no-reset-on-success flag Keep form values after a successful submission instead of resetting.
data-no-close-on-success flag Keep the parent modal open after a successful submission.
data-append-to string CSS selector for a select element to append the new option to on success.