bayareawebpro / laravel-multistep-forms
针对Laravel的多步骤表单构建器。
v1.3.0
2024-06-07 00:00 UTC
Requires
- php: ^8.2
- illuminate/contracts: ^11.0
- illuminate/http: ^11.0
- illuminate/session: ^11.0
- illuminate/support: ^11.0
- illuminate/validation: ^11.0
Requires (Dev)
- larastan/larastan: ^2.0
- orchestra/testbench: ^9.0
- phpunit/phpunit: ^11.0
README
https://packagist.org.cn/packages/bayareawebpro/laravel-multistep-forms
多步骤表单构建器是一个可以由控制器返回的“责任”类。
- 指定要使用的视图,或者使用JSON无头方式与JavaScript框架一起使用。
- 使用简单的数组配置每个步骤的规则、消息和辅助数据。
- 多次提交到同一路由,将每个验证过的请求合并到命名空间会话键中。
- 在验证前后挂钩到每个步骤,以与表单交互或返回响应。
安装
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. }) ;
创建新实例
使用可选的视图和数据数组创建构建器类的实例。您应该始终为表单会话设置namespace
,以避免与其他使用会话存储的应用程序部分发生冲突。
GET
请求将加载表单状态和数据,对于已保存的当前步骤或回退到步骤1。POST
、PUT
、PATCH
等...将验证和处理任何步骤的请求,并继续到下一个配置的步骤。DELETE
将重置会话状态并返回(blade),或返回JsonResponse
。- 通过
canNavigateBack
方法启用向后导航(通过get参数)。
<?php use BayAreaWebPro\MultiStepForms\MultiStepForm; $form = MultiStepForm::make('onboarding.start', [ 'title' => 'Setup your account' ]); $form->namespaced('onboarding'); $form->canNavigateBack(true);
配置步骤
定义步骤的规则、消息和数据。数据将与在make
方法中定义的任何视图数据合并,并包含在JsonResponse
中。
** 使用Closure
按键懒加载数据。
使用数组:
$form->addStep(2, [ 'rules' => [ 'role' => 'required|string' ], 'messages' => [ 'role.required' => 'Your name is required.' ], 'data' => [ 'roles' => fn() => Role::query()..., ], ])
或使用可调用的类(推荐)
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));
BeforeStep / OnStep钩子
定义一个回调,在验证步骤之前触发。步骤编号为*表示所有步骤。
- 使用步骤整数或星号(*)表示所有步骤。
- 您可以从这些钩子返回响应。
$form->beforeStep('*', function(MultiStepForm $form){ // }); $form->onStep('*', function(MultiStepForm $form){ // }); $form->onComplete(function(MultiStepForm $form){ // });
处理上传的文件
指定一个回调,用于将上传的文件转换为路径。
use Illuminate\Http\UploadedFile; $form->beforeSave(function(array $data){ if($data['avatar'] instanceof UploadedFile){ $data['avatar'] = $data['avatar']->store('avatars'); } return $data; });
重置/清除表单
- Ajax:向表单路由提交DELETE请求。
- Blade:使用额外的提交按钮,传递布尔值(truthy)。
<button type="submit" name="reset" value="1">Reset</button>
JSON响应模式
返回的响应将有两个属性
{ "form": { "form_step": 1 }, "data": {} }
公共辅助方法
stepConfig
获取当前步骤配置(默认),或传递整数以获取特定步骤
$form->stepConfig(2): Collection
getValue
获取字段值(会话/旧输入)或回退
$form->getValue('name', 'John Doe'): mixed
setValue
设置字段值并将其存储在会话中
$form->setValue('name', 'Jane Doe'): MultiStepForm
save
直接合并和保存键/值数组到会话(不触发beforeSaveCallback
)
$form->save(['name' => 'Jane Doe']): MultiStepForm
reset
将表单状态重置为默认值,可选地传递一个数据数组以初始化。
$form->reset(['name' => 'Jane Doe']): MultiStepForm
withData
将非表单数据添加到所有视图和响应中
$form->withData(['date' => now()->toDateString()]);
currentStep
获取当前保存的步骤编号
$form->currentStep(): int
requestedStep
获取传入客户端请求的步骤编号
$form->requestedStep(): int
isStep
当前步骤是提供的步骤吗
$form->isStep(3): bool
prevStepUrl
获取上一个步骤URL。
$form->prevStepUrl(): string|null
lastStep
获取最后一步编号
$form->lastStep(): int
isLastStep
当前步骤是否为最后一步
$form->isLastStep(): bool
isPast,isActive,isFuture
// 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
Blade 示例
数据将被注入到视图以及表单本身,允许您访问表单值和其他辅助方法。
<?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>
Vue 示例
如果没有指定视图或请求偏好 JSON,表单状态和数据将以 JSON 格式返回。您可以将这两种技术结合起来,在 blade 中使用 Vue。
<v-form action="{{ route('submit') }}"> <template v-slot:default="{form, options, errors, reset, back}"> <h1 class="font-black my-3"> @{{ options.title }} </h1> <p v-if="options.message" role="alert" class="bg-gray-200 p-4 my-5 font-bold text-blue-500"> @{{ options.message }} </p> <template v-if="form.form_step < 4"> <a @click="back(1)" :class="{'text-blue-500': form.form_step > 1, 'font-bold': form.form_step === 1}"> Step 1 </a> <a @click="back(2)" :class="{'text-blue-500': form.form_step > 2, 'font-bold': form.form_step === 2}"> Step 2 </a> <a @click="back(3)" :class="{'text-blue-500': form.form_step > 3, 'font-bold': form.form_step === 3}"> Step 3 </a> </template> <template v-if="form.form_step === 1"> <v-input name="name" label="Name" :errors="errors" v-model="form.name"> </v-input> <v-select name="name" label="Name" :errors="errors" :options="options.roles" v-model="form.role"> </v-select> <x-action>Continue</x-action> </template> <template v-if="form.form_step === 2"> <v-input name="email" label="Email" :errors="errors" v-model="form.email"> </v-input> <v-input name="phone" label="Phone" :errors="errors" v-model="form.phone"> </v-input> <x-action>Continue</x-action> </template> <template v-if="form.form_step === 3"> <v-input name="bio" label="Bio" :errors="errors" v-model="form.bio"> </v-input> <v-input name="notify" label="Notify" :errors="errors" v-model="form.notify"> </v-input> <x-action>Continue</x-action> </template> <template v-if="form.form_step === 4"> <h3>Review Submission</h3> <p> Name: @{{ form.name }}<br> Role: @{{ form.role }}<br> Email: @{{ form.email }}<br> Phone: @{{ form.phone }}<br> </p> <x-action>Save</x-action> <x-action @click="reset">Reset</x-action> </template> <template v-if="form.form_step === 5"> <x-action>Done</x-action> </template> </template> </v-form>
示例表单组件
<script> export default { name: 'Form', props: ['action'], 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.action, {params}) .then(this.onResponse) .catch(this.onError) }, submit() { axios .post(this.action, this.form) .then(this.onResponse) .catch(this.onError) }, onError({response}) { this.errors = (response.data.errors || response.data.exception) }, onResponse({data}) { this.errors = {} this.options = (data.data || {}) this.form = (data.form || {}) }, }, created() { this.fetch() } } </script> <template> <form @submit.prevent="submit"> <slot :reset="reset" :back="back" :form="form" :options="options" :errors="errors"/> </form> </template>
示例输入组件
<script> export default { name: "Input", props: ['name', 'label', 'value', 'errors'], computed: { field: { get() { return this.value }, set(val) { return this.$emit('input', val) } } } } </script> <template> <label class="block my-4"> <span class="text-gray-700 font-bold"> {{ label || name }} </span> <input type="text" v-model="field" class="form-input block w-full mt-2"> <div v-if="errors[name]" class="text-red-500 text-xs my-2"> {{ errors[name][0] }} </div> </label> </template>
示例选择组件
<script> export default { name: "Select", props: ['name', 'label', 'value', 'errors', 'options'], computed: { field: { get() { return this.value }, set(val) { return this.$emit('input', val) } } } } </script> <template> <label class="block"> <span class="text-gray-700">{{ label || name }}</span> <select v-model="field" class="form-select mt-1 block w-full"> <option disabled value="">Please select one</option> <option v-for="option in options" :value="option"> {{ option }} </option> </select> <div v-if="errors[name]" class="text-red-500 text-xs my-2"> {{ errors[name][0] }} </div> </label> </template>