├── .github
└── workflows
│ └── ci.yaml
├── .gitignore
├── LICENSE
├── README.md
├── artisan
├── composer.json
├── phpstan.neon.dist
├── phpunit.xml
├── src
└── MultiStepForm.php
└── tests
├── Fixtures
├── Invoke.php
└── views
│ └── form.blade.php
├── TestCase.php
└── Unit
├── DataTest.php
├── DeleteTest.php
├── GuardTest.php
├── HooksTest.php
├── StepFlow.php
└── ValidationTest.php
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: ci
2 | env:
3 | XDEBUG_MODE: 'coverage'
4 | on:
5 | push:
6 | branches:
7 | - master
8 | paths-ignore:
9 | - 'README.md'
10 | - 'LICENSE'
11 | pull_request:
12 | branches:
13 | - master
14 | paths-ignore:
15 | - 'README.md'
16 | - 'LICENSE'
17 | jobs:
18 | build:
19 | runs-on: ubuntu-24.04
20 | steps:
21 | - uses: actions/checkout@v1
22 | with:
23 | fetch-depth: 1
24 | - name: Cache Composer
25 | uses: actions/cache@v4
26 | with:
27 | path: vendor
28 | key: ${{ runner.OS }}-build-${{ hashFiles('**/composer.lock') }}
29 | - name: Composer Dependencies
30 | run: composer install --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist
31 | - name: Lint
32 | run: composer lint
33 | - name: Unit Tests
34 | run: composer test
35 | - name: Codecov
36 | uses: codecov/codecov-action@v1
37 | with:
38 | token: ${{ secrets.CODECOV_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/
2 | /.phpunit.cache/
3 | /.phpunit.result.cache
4 | /build/
5 | /vendor/
6 | /composer.lock
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Dan Alvidrez
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel MultiStep Forms
2 |
3 | 
4 | 
5 | 
6 | 
7 | 
8 |
9 | > https://packagist.org/packages/bayareawebpro/laravel-multistep-forms
10 |
11 | Multistep Form Builder is a "[responsable](https://laravel-news.com/laravel-5-5-responsable)" class that can be returned from controllers.
12 |
13 | * Specify a view to use Blade or go headless with JSON for use with Javascript frameworks.
14 | * Configure the rules, messages and supporting data for each step with simple arrays.
15 | * Submit to the same route multiple times to merge each validated request into a namespaced session key.
16 | * Hook into each step **before** or **after** validation to interact with the form or return a response.
17 |
18 | ## Installation
19 |
20 | ```shell script
21 | composer require bayareawebpro/laravel-multistep-forms
22 | ```
23 |
24 | ### Example Usage
25 |
26 | ```php
27 | 'MultiStep Form'
34 | ])
35 |
36 | // Namespace the session data.
37 | ->namespaced('my-session-key')
38 |
39 | // Allow backwards navigation via get request. ?form_step=x
40 | ->canNavigateBack(true)
41 |
42 | // Tap invokable Class __invoke(Form $form)
43 | ->tap(new InvokableClass)
44 |
45 | // Before x step validation...
46 | ->beforeStep(1, function (MultiStepForm $form) {
47 | // Maybe return early or redirect?
48 | })
49 | // Before all step validation...
50 | ->beforeStep('*', function (MultiStepForm $form) {
51 | // Maybe return early or redirect?
52 | })
53 |
54 | // Validate Step 1
55 | ->addStep(1, [
56 | 'rules' => ['name' => 'required'],
57 | 'messages' => ['name.required' => 'Your name is required.'],
58 | ])
59 |
60 | // Validate Step 2
61 | ->addStep(2, [
62 | 'rules' => ['role' => 'required|string'],
63 | 'data' => ['roles' => fn()=>Role::forSelection()] // Lazy Loaded Closure
64 | ])
65 |
66 | // Add non-validated step...
67 | ->addStep(3,[
68 | 'data' => ['message' => "Great Job, Your Done!"]
69 | ])
70 |
71 | // After step validation...
72 | ->onStep(3, function (MultiStepForm $form) {
73 | // Specific step, logic if needed.
74 | })
75 | ->onStep('*', function (MultiStepForm $form) {
76 | // All steps, logic if needed.
77 | })
78 |
79 | // Modify data before saved to session after each step.
80 | ->beforeSave(function(array $data) {
81 |
82 | // Transform non-serializable objects to paths, array data etc...
83 | return $data;
84 | })
85 |
86 | // Modify data before saved to session after each step.
87 | ->onComplete(function(MultiStepForm $form) {
88 |
89 | // Final submission logic.
90 | })
91 | ;
92 | ```
93 |
94 | ---
95 |
96 | ### Make New Instance
97 |
98 | Make a new instance of the builder class with optional view and data array. You
99 | should always set the `namespace` for the form session to avoid conflicts with
100 | other parts of your application that use the session store.
101 |
102 | * `GET` requests will load the form state and data for the saved current step or fallback to step 1.
103 | * `POST`,`PUT`,`PATCH` etc... will validate and process the request for any step and proceed to the next configured step.
104 | * `DELETE` will reset the session state and redirect back (blade), or return a `JsonResponse`.
105 | * Backwards navigation (via get param) can be enabled via the `canNavigateBack` method.
106 |
107 | ```php
108 | 'Setup your account'
114 | ]);
115 |
116 | $form->namespaced('onboarding');
117 | $form->canNavigateBack(true);
118 | ```
119 |
120 | ---
121 |
122 | ### Configure Steps
123 |
124 | Define the rules, messages and data for the step. Data will be merged
125 | with any view data defined in the `make` method and be included in the `JsonResponse`.
126 |
127 | ** Use a `Closure` to lazy load data per-key.
128 |
129 | **Use an array**:
130 |
131 | ```php
132 | $form->addStep(2, [
133 | 'rules' => [
134 | 'role' => 'required|string'
135 | ],
136 | 'messages' => [
137 | 'role.required' => 'Your name is required.'
138 | ],
139 | 'data' => [
140 | 'roles' => fn() => Role::query()...,
141 | ],
142 | ])
143 | ```
144 |
145 | **Or use an invokable class** (recommended)
146 |
147 | ```php
148 | use BayAreaWebPro\MultiStepForms\MultiStepForm;
149 |
150 | class ProfileStep
151 | {
152 | public function __construct(private int $step)
153 | {
154 | //
155 | }
156 |
157 | public function __invoke(MultiStepForm $form)
158 | {
159 | $form->addStep($this->step, [
160 | 'rules' => [
161 | 'name' => 'required|string'
162 | ],
163 | 'messages' => [
164 | 'name.required' => 'Your name is required.'
165 | ],
166 | 'data' => [
167 | 'placeholders' => [
168 | 'name' => 'Enter your name.'
169 | ]
170 | ],
171 | ]);
172 | }
173 | }
174 | ```
175 |
176 | ```php
177 | $form->tap(new ProfileStep(1));
178 | ```
179 |
180 | ---
181 |
182 | ### BeforeStep / OnStep Hooks
183 |
184 | Define a callback to fired **before** a step has been validated. Step Number or * for all.
185 |
186 | - Use a step integer, or asterisk (*) for all steps.
187 | - You can return a response from these hooks.
188 |
189 | ```php
190 | $form->beforeStep('*', function(MultiStepForm $form){
191 | //
192 | });
193 | $form->onStep('*', function(MultiStepForm $form){
194 | //
195 | });
196 | $form->onComplete(function(MultiStepForm $form){
197 | //
198 | });
199 | ```
200 |
201 | ### Handle UploadedFiles
202 |
203 | Specify a callback used to transform UploadedFiles into paths.
204 |
205 | ```php
206 | use Illuminate\Http\UploadedFile;
207 |
208 | $form->beforeSave(function(array $data){
209 | if($data['avatar'] instanceof UploadedFile){
210 | $data['avatar'] = $data['avatar']->store('avatars');
211 | }
212 | return $data;
213 | });
214 | ```
215 |
216 | ### Reset / Clear Form
217 |
218 | - Ajax: Submit a DELETE request to the form route.
219 | - Blade: Use an additional submit button that passes a boolean (truthy) value.
220 |
221 | ```
222 |
223 | ```
224 |
225 | ### JSON Response Schema
226 |
227 | The response returned will have two properties:
228 |
229 | ```json
230 | {
231 | "form": {
232 | "form_step": 1
233 | },
234 | "data": {}
235 | }
236 | ```
237 |
238 | ### Public Helper Methods
239 |
240 |
241 | #### stepConfig
242 | Get the current step configuration (default), or pass an integer for a specific step:
243 | ```php
244 | $form->stepConfig(2): Collection
245 | ```
246 |
247 | #### getValue
248 | Get a field value (session / old input) or fallback:
249 |
250 | ```php
251 | $form->getValue('name', 'John Doe'): mixed
252 | ```
253 |
254 | #### setValue
255 | Set a field value and store in the session:
256 | ```php
257 | $form->setValue('name', 'Jane Doe'): MultiStepForm
258 | ```
259 |
260 | #### save
261 | Merge and save key/values array directly to the session (does not fire `beforeSaveCallback`):
262 |
263 | ```php
264 | $form->save(['name' => 'Jane Doe']): MultiStepForm
265 | ```
266 |
267 | #### reset
268 |
269 | Reset the form state to defaults passing an optional array of data to seed.
270 |
271 | ```php
272 | $form->reset(['name' => 'Jane Doe']): MultiStepForm
273 | ```
274 |
275 | #### withData
276 | Add additional non-form data to all views and responses:
277 |
278 | ```php
279 | $form->withData(['date' => now()->toDateString()]);
280 | ```
281 |
282 | #### currentStep
283 | Get the current saved step number:
284 |
285 | ```php
286 | $form->currentStep(): int
287 | ```
288 |
289 | #### requestedStep
290 | Get the incoming client-requested step number:
291 |
292 | ```php
293 | $form->requestedStep(): int
294 | ```
295 |
296 | #### isStep
297 | Is the current step the provided step:
298 |
299 | ```php
300 | $form->isStep(3): bool
301 | ```
302 |
303 | #### prevStepUrl
304 | Get the previous step url.
305 |
306 | ```php
307 | $form->prevStepUrl(): string|null
308 | ```
309 |
310 | #### lastStep
311 | Get the last step number:
312 |
313 | ```php
314 | $form->lastStep(): int
315 | ```
316 |
317 | #### isLastStep
318 | Is the current step the last step:
319 |
320 | ```php
321 | $form->isLastStep(): bool
322 | ```
323 |
324 | #### isPast,isActive,isFuture
325 |
326 | ```php
327 | // Boolean Usage
328 | $form->isPast(2): bool
329 | $form->isActive(2): bool
330 | $form->isFuture(2): bool
331 |
332 | // Usage as HTML Class Helpers
333 | $form->isPast(2, 'truthy-class', 'falsy-class'): string
334 | $form->isActive(2, 'truthy-class', 'falsy-class'): string
335 | $form->isFuture(2, 'truthy-class', 'falsy-class'): string
336 | ```
337 |
338 | ---
339 |
340 | ### Blade Example
341 |
342 | Data will be injected into the view as well as the form itself allowing you to access the form values and other helper methods.
343 |
344 | ```php
345 | namespaced('onboarding');
350 | $form->canNavigateBack(true);
351 | ```
352 |
353 | ```blade
354 |
410 | ```
411 |
412 | ### Vue Example
413 |
414 | Form state and data will be returned as JSON when no view is
415 | specified or the request prefers JSON. You can combine both
416 | techniques to use Vue within blade as well.
417 |
418 | ```html
419 |
420 |
421 |
422 |
423 |