Responsable MultiStep Form Builder for Laravel.
bayareawebpro/laravel-multistep-forms is a Laravel package for responsable multistep form builder for laravel..
It currently has 96 GitHub stars and 8.034 downloads on Packagist (latest version v1.3.2).
Install it with composer require bayareawebpro/laravel-multistep-forms.
Discover more Laravel packages by bayareawebpro
or browse all Laravel packages to compare alternatives.
Last updated
https://packagist.org/packages/bayareawebpro/laravel-multistep-forms
Multistep Form Builder is a "responsable" class that can be returned from controllers.
composer require bayareawebpro/laravel-multistep-forms
<?php
use BayAreaWebPro\MultiStepForms\MultiStepForm;
// Render a view with data.
return Form::make('my-form', [
'title' => 'MultiStep Form'
])
// Namespace the session data.
->namespaced('my-session-key')
// Allow backwards navigation via get request. ?form_step=x
->canNavigateBack(true)
// Tap invokable Class __invoke(Form $form)
->tap(new InvokableClass)
// Before x step validation...
->beforeStep(1, function (MultiStepForm $form) {
// Maybe return early or redirect?
})
// Before all step validation...
->beforeStep('*', function (MultiStepForm $form) {
// Maybe return early or redirect?
})
// Validate Step 1
->addStep(1, [
'rules' => ['name' => 'required'],
'messages' => ['name.required' => 'Your name is required.'],
])
// Validate Step 2
->addStep(2, [
'rules' => ['role' => 'required|string'],
'data' => ['roles' => fn()=>Role::forSelection()] // Lazy Loaded Closure
])
// Add non-validated step...
->addStep(3,[
'data' => ['message' => "Great Job, Your Done!"]
])
// After step validation...
->onStep(3, function (MultiStepForm $form) {
// Specific step, logic if needed.
})
->onStep('*', function (MultiStepForm $form) {
// All steps, logic if needed.
})
// Modify data before saved to session after each step.
->beforeSave(function(array $data) {
// Transform non-serializable objects to paths, array data etc...
return $data;
})
// Modify data before saved to session after each step.
->onComplete(function(MultiStepForm $form) {
// Final submission logic.
})
;
Make a new instance of the builder class with optional view and data array. You
should always set the namespace for the form session to avoid conflicts with
other parts of your application that use the session store.
GET requests will load the form state and data for the saved current step or fallback to step 1.POST,PUT,PATCH etc... will validate and process the request for any step and proceed to the next step.DELETE will reset the session state and redirect back (blade), or return a JsonResponse.canNavigateBack method.<?php
use BayAreaWebPro\MultiStepForms\MultiStepForm;
$form = MultiStepForm::make('onboarding.start', [
'title' => 'Setup your account'
]);
$form->namespaced('onboarding');
$form->canNavigateBack(true);
Define the rules, messages and data for the step. Data will be merged
with any view data defined in the make method and be included in the JsonResponse.
** Use a Closure to lazy load data per-key.
Use an array:
$form->addStep(2, [
'rules' => [
'role' => 'required|string'
],
'messages' => [
'role.required' => 'Your name is required.'
],
'data' => [
'roles' => fn() => Role::query()...,
],
])
Or use an invokable class (recommended)
use BayAreaWebPro\MultiStepForms\MultiStepForm;
class ProfileStep
{
public function __construct(private int $step)
{
//
}
public function __invoke(MultiStepForm $form)
{
$form->addStep($this->step, [
'rules' => [
'name' => 'required|string'
],
'messages' => [
'name.required' => 'Your name is required.'
],
'data' => [
'placeholders' => [
'name' => 'Enter your name.'
]
],
]);
}
}
$form->tap(new ProfileStep(1));
Define a callback to fired before a step has been validated. Step Number or * for all.
$form->beforeStep('*', function(MultiStepForm $form){
//
});
$form->onStep('*', function(MultiStepForm $form){
//
});
$form->onComplete(function(MultiStepForm $form){
//
});
Specify a callback used to transform UploadedFiles into paths.
use Illuminate\Http\UploadedFile;
$form->beforeSave(function(array $data){
if($data['avatar'] instanceof UploadedFile){
$data['avatar'] = $data['avatar']->store('avatars');
}
return $data;
});
<button type="submit" name="reset" value="1">Reset</button>
The response returned will have two properties:
{
"form": {
"form_step": 1
},
"data": {}
}
Get the current step configuration (default), or pass an integer for a specific step:
$form->stepConfig(2): Collection
Get a field value (session / old input) or fallback:
$form->getValue('name', 'John Doe'): mixed
Set a field value and store in the session:
$form->setValue('name', 'Jane Doe'): MultiStepForm
Merge and save key/values array directly to the session (does not fire beforeSaveCallback):
$form->save(['name' => 'Jane Doe']): MultiStepForm
Reset the form state to defaults passing an optional array of data to seed.
$form->reset(['name' => 'Jane Doe']): MultiStepForm
Add additional non-form data to all views and responses:
$form->withData(['date' => now()->toDateString()]);
Get the current saved step number:
$form->currentStep(): int
Get the incoming client-requested step number:
$form->requestedStep(): int
Is the current step the provided step:
$form->isStep(3): bool
Get the previous step url.
$form->prevStepUrl(): string|null
Get the last step number:
$form->lastStep(): int
Is the current step the last step:
$form->isLastStep(): bool
// Boolean Usage
$form->isPast(2): bool
$form->isActive(2): bool
$form->isFuture(2): bool
// Usage as HTML Class Helpers
$form->isPast(2, 'truthy-class', 'falsy-class'): string
$form->isActive(2, 'truthy-class', 'falsy-class'): string
$form->isFuture(2, 'truthy-class', 'falsy-class'): string
Data will be injected into the view as well as the form itself allowing you to access the form values and other helper methods.
<?php
use BayAreaWebPro\MultiStepForms\MultiStepForm as Form;
$form = Form::make('my-view', $data);
$form->namespaced('onboarding');
$form->canNavigateBack(true);
<form method="post" action="{{ route('submit') }}">
<input type="hidden" name="form_step" value="{{ $form->currentStep() }}">
@csrf
<a
href="{{ route('submit', ['form_step' => 1]) }}"
class="{{ $form->isPast(1, 'text-blue-500', $form->isActive(1, 'font-bold', 'disabled')) }}">
Step 1
</a>
<a
href="{{ route('submit', ['form_step' => 2]) }}"
class="{{ $form->isPast(2, 'text-blue-500', $form->isActive(2, 'font-bold', 'disabled')) }}">
Step 2
</a>
<a
href="{{ route('submit', ['form_step' => 3]) }}"
class="{{ $form->isPast(3, 'text-blue-500', $form->isActive(3, 'font-bold', 'disabled')) }}">
Step 3
</a>
@switch($form->currentStep())
@case(1)
<label>Name</label>
<input type="text" name="name" value="{{ $form->getValue('name') }}">
@error('name')
<p>{{ $errors->first('name') }}</p>
@enderror
@break
@case(2)
<label>Role</label>
<input type="text" name="role" value="{{ $form->getValue('role') }}">
@error('role')
<p>{{ $errors->first('role') }}</p>
@enderror
@break
@case(3)
<p>Review your submission:</p>
<p>
Name: {{ $form->getValue('name') }}<br>
Role: {{ $form->getValue('role') }}<br>
</p>
@break
@endswitch
@if($form->isLastStep())
<button type="submit" name="submit">Save</button>
<button type="submit" name="reset" value="1">Reset</button>
@else
<button type="submit" name="submit">Continue</button>
@endif
</form>
Form state and data will be returned as JSON when no view is specified or the request prefers JSON. You can combine both techniques to use Vue within blade as well.
<v-form route="{{ route('onboarding') }}">
<template v-slot:default="{form, options, errors, reset, back}">
<nav aria-label="Steps" v-if="form.form_step < 4">
<a
@click="back(1)"
:class="{'text-gray-500': form.form_step > 1, 'text-blue-500': form.form_step === 1}">
Step 1
</a>
<a
@click="back(2)"
:class="{'text-gray-500': form.form_step > 2, 'text-blue-500': form.form_step === 2}">
Step 2
</a>
<a
@click="back(3)"
:class="{'text-gray-500': form.form_step > 3, 'text-blue-500': form.form_step === 3}">
Step 3
</a>
</nav>
<template v-if="form.form_step === 1">
<v-input
name="name"
label="Name"
:errors="errors"
v-model="form.name">
</v-input>
</template>
<template v-if="form.form_step === 2">
<v-input
name="email"
label="Email"
:errors="errors"
v-model="form.email">
</v-input>
</template>
<template v-if="form.form_step === 3">
<v-input
name="phone"
label="Phone"
:errors="errors"
v-model="form.phone">
</v-input>
</template>
<template v-if="form.form_step === 4">
<ul>
<li>Name: @{{ form.name }}</li>
<li>Email: @{{ form.email }}</li>
<li>Phone: @{{ form.phone }}</li>
</ul>
</template>
<template v-if="form.form_step <= 4">
<button type="submit">Save</button>
<button type="button" @click="reset">Reset</button>
</template>
<template v-else>
<button type="submit">Done</button>
</template>
</template>
</v-form>
<script>
export default {
name: 'v-form',
props: ['route'],
data: () => ({
errors: {},
options: {},
form: {form_step: 1},
}),
methods: {
reset() {
this.form.reset = 1
this.submit()
},
back(step) {
if (step < this.form.form_step) {
this.fetch({form_step: step})
}
},
fetch(params = {}) {
axios
.get(this.route, {params})
.then(this.onResponse)
.catch(this.onError)
},
submit() {
axios
.post(this.route, this.form)
.then(this.onResponse)
.catch(this.onError)
},
onError({response}) {
this.errors = response.data.errors
},
onResponse({data}) {
this.errors = {}
this.options = (data.data || {})
this.form = (data.form || {})
},
},
created() {
this.fetch()
}
}
</script>
<template>
<form @submit.prevent="submit">
<slot
:form="form"
:errors="errors"
:options="options"
:back="back"
:reset="reset"
/>
</form>
</template>
<script>
export default {
name: "v-form-input",
emits: ['update:modelValue'],
props: ['name', 'modelValue', 'errors'],
computed: {
field: {
get() {
return this.modelValue
},
set(val) {
return this.$emit('update:modelValue', val)
}
}
}
}
</script>
<template>
<input
type="text"
:name="name"
v-model="field"
class="form-input"
/>
<div v-if="errors[name]">
{{ errors[name][0] }}
</div>
</template>