├── LICENSE.md ├── README.md ├── composer.json ├── config └── livewire-wizard.php ├── resources └── views │ ├── step-header.blade.php │ ├── steps-footer.blade.php │ ├── steps-header.blade.php │ └── wizard.blade.php └── src ├── Components └── Step.php ├── Concerns ├── BelongsToLivewire.php ├── HasHooks.php ├── HasState.php └── HasSteps.php ├── Contracts └── WizardForm.php ├── LivewireWizardServiceProvider.php └── WizardComponent.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Vildan Bina 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Stable Version](https://poser.pugx.org/vildanbina/livewire-wizard/v)](https://packagist.org/packages/vildanbina/livewire-wizard) 2 | [![Total Downloads](https://poser.pugx.org/vildanbina/livewire-wizard/downloads)](https://packagist.org/packages/vildanbina/livewire-wizard) 3 | [![Latest Unstable Version](https://poser.pugx.org/vildanbina/livewire-wizard/v/unstable)](https://packagist.org/packages/vildanbina/livewire-wizard) 4 | [![License](https://poser.pugx.org/vildanbina/livewire-wizard/license)](https://packagist.org/packages/vildanbina/livewire-wizard) 5 | [![PHP Version Require](https://poser.pugx.org/vildanbina/livewire-wizard/require/php)](https://packagist.org/packages/vildanbina/livewire-wizard) 6 | 7 | A dynamic Laravel Livewire component for multi steps form. 8 | 9 | ![Multi steps form](https://user-images.githubusercontent.com/51203303/155848196-e3569891-cb63-499d-8079-a63a30925b77.png) 10 | 11 | ## Installation 12 | 13 | You can install the package via composer: 14 | 15 | ``` bash 16 | composer require vildanbina/livewire-wizard 17 | ``` 18 | 19 | For UI design this package require [WireUI package](https://livewire-wireui.com) for details. 20 | 21 | ## Alpine 22 | 23 | Livewire Wizard requires [Alpine](https://github.com/alpinejs/alpine). You can use the official CDN to quickly include Alpine: 24 | 25 | ```html 26 | 27 | 28 | 29 | 30 | 31 | ``` 32 | 33 | ## TailwindCSS 34 | 35 | The base modal is made with TailwindCSS. If you use a different CSS framework I recommend that you publish the modal template and change the markup to include the required classes for your CSS framework. 36 | 37 | ```shell 38 | php artisan vendor:publish --tag=livewire-wizard-views 39 | ``` 40 | 41 | ## Usage 42 | 43 | ### Creating a wizard form 44 | 45 | You can create livewire component `php artisan make:livewire UserWizard` to make the initial Livewire component. Open your component class and make sure it extends the `WizardComponent` class: 46 | 47 | ```php 48 | userId); 66 | } 67 | } 68 | ``` 69 | 70 | When you need to display wizard form, based on above example we need to pass `$userId` value and to display wizard form: 71 | 72 | ```html 73 | 74 | ``` 75 | 76 | Or when you want to create new user, let blank `user-id` attribute, or don't put that. 77 | 78 | When you want to reset form, ex. To reset to the first step, and clear filled fields. You can use: 79 | 80 | ```php 81 | $wizardFormInstance->resetForm(); 82 | ``` 83 | 84 | When you want to have current step instance. You can use: 85 | 86 | ```php 87 | $wizardFormInstance->getCurrentStep(); 88 | ``` 89 | 90 | When you want to go to specific step. You can use: 91 | 92 | ```php 93 | $wizardFormInstance->setStep($step); 94 | ``` 95 | 96 | Or, you want to go in the next step: 97 | 98 | ```php 99 | $wizardFormInstance->goToNextStep(); 100 | ``` 101 | 102 | Or, you want to go in the prev step: 103 | 104 | ```php 105 | $wizardFormInstance->goToPrevStep(); 106 | ``` 107 | 108 | ### Creating a wizard step 109 | 110 | You can create wizard form step. Open or create your step class (at `App\Steps` folder) and make sure it extends the `Step` class: 111 | 112 | ```php 113 | mergeState([ 131 | 'name' => $this->model->name, 132 | 'email' => $this->model->email, 133 | ]); 134 | } 135 | 136 | /* 137 | * Step icon 138 | */ 139 | public function icon(): string 140 | { 141 | return 'check'; 142 | } 143 | 144 | /* 145 | * When Wizard Form has submitted 146 | */ 147 | public function save($state) 148 | { 149 | $user = $this->model; 150 | 151 | $user->name = $state['name']; 152 | $user->email = $state['email']; 153 | 154 | $user->save(); 155 | } 156 | 157 | /* 158 | * Step Validation 159 | */ 160 | public function validate() 161 | { 162 | return [ 163 | [ 164 | 'state.name' => ['required', Rule::unique('users', 'name')->ignoreModel($this->model)], 165 | 'state.email' => ['required', Rule::unique('users', 'email')->ignoreModel($this->model)], 166 | ], 167 | [], 168 | [ 169 | 'state.name' => __('Name'), 170 | 'state.email' => __('Email'), 171 | ], 172 | ]; 173 | } 174 | 175 | /* 176 | * Step Title 177 | */ 178 | public function title(): string 179 | { 180 | return __('General'); 181 | } 182 | } 183 | ``` 184 | 185 | **Note:** Remember to use the prefix `state.` in the `wire:model` attribute in views, for example: `wire:model="state.name"` 186 | 187 | 188 | 189 | 190 | 191 | In Step class, you can use livewire hooks example: 192 | 193 | ```php 194 | use Vildanbina\LivewireWizard\Components\Step; 195 | 196 | class General extends Step 197 | { 198 | public function onStepIn($newStep) 199 | { 200 | // Something you want 201 | } 202 | 203 | public function onStepOut($oldStep) 204 | { 205 | // Something you want 206 | } 207 | 208 | public function updating($name, $value) 209 | { 210 | // Something you want 211 | } 212 | 213 | public function updatingState($name, $value) 214 | { 215 | // Something you want 216 | } 217 | 218 | public function updated($name, $value) 219 | { 220 | // Something you want 221 | } 222 | 223 | public function updatedState($name, $value) 224 | { 225 | // Something you want 226 | } 227 | } 228 | ``` 229 | 230 | Each step need to have view, you can pass view path in `$view` property. 231 | 232 | After create step class, you need to put that step to wizard form: 233 | 234 | ```php 235 | isValid(); 3 | $isFailedStep = $stepInstance->validationFails; 4 | $stepIsGreaterOrEqualThan = $this->stepIsGreaterOrEqualThan($stepInstance->getOrder()); 5 | @endphp 6 |
7 |
8 | @if(!$loop->first) 9 |
10 |
11 |
$stepIsGreaterOrEqualThan && !$isFailedStep, 15 | 'bg-red-300' => $isFailedStep, 16 | 'w-full' => $isFailedStep || $stepIsGreaterOrEqualThan, 17 | 'w-0' => !($isFailedStep || $stepIsGreaterOrEqualThan) 18 | ]) 19 | >
20 |
21 |
22 | @endif 23 | 24 |
25 | 31 |
32 |
33 |
{{ $stepInstance->title() }}
34 |
35 | -------------------------------------------------------------------------------- /resources/views/steps-footer.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if($this->hasNextStep()) 3 | 4 | @else 5 | 6 | @endif 7 | @if($this->hasPrevStep()) 8 | 9 | @endif 10 |
11 | -------------------------------------------------------------------------------- /resources/views/steps-header.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | @foreach($stepInstances as $stepInstance) 4 | @include('livewire-wizard::step-header') 5 | @endforeach 6 |
7 |
8 | -------------------------------------------------------------------------------- /resources/views/wizard.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | @include('livewire-wizard::steps-header') 4 |
5 | 6 | {{ $this->getCurrentStep() }} 7 |
8 | 9 | @include('livewire-wizard::steps-footer') 10 |
11 |
12 | -------------------------------------------------------------------------------- /src/Components/Step.php: -------------------------------------------------------------------------------- 1 | setLivewire($livewire) 27 | ->setModel($livewire->getModel()); 28 | } 29 | 30 | public static function make(WizardForm $livewire): static 31 | { 32 | return new static($livewire); 33 | } 34 | 35 | public function getModel(): ?Model 36 | { 37 | return $this->model; 38 | } 39 | 40 | public function setModel(?Model $model): Step 41 | { 42 | $this->model = $model; 43 | return $this; 44 | } 45 | 46 | public function icon(): string 47 | { 48 | return 'check'; 49 | } 50 | 51 | public function isValid(): bool 52 | { 53 | if (method_exists($this, 'validate')) { 54 | return !validator(['state' => $this->livewire->state], ...$this->validate())->fails(); 55 | } 56 | 57 | return true; 58 | } 59 | 60 | public function setState(array $state = []) 61 | { 62 | $this->getLivewire()->state = $state; 63 | } 64 | 65 | public function mergeState(array $state = []) 66 | { 67 | $this->getLivewire()->mergeState($state); 68 | } 69 | 70 | public function putState($key, $value = null, $default = null) 71 | { 72 | $this->getLivewire()->putState($key, $value, $default); 73 | } 74 | 75 | abstract public function title(): string; 76 | 77 | public function getOrder(): ?int 78 | { 79 | return $this->order; 80 | } 81 | 82 | public function setOrder(int $order): static 83 | { 84 | $this->order = $order; 85 | return $this; 86 | } 87 | 88 | public function setView(string $view): static 89 | { 90 | $this->view = $view; 91 | 92 | return $this; 93 | } 94 | 95 | public function toHtml(): string 96 | { 97 | return $this->render()->render(); 98 | } 99 | 100 | public function render(): View 101 | { 102 | return view($this->getView(), array_merge($this->data(), [ 103 | 'container' => $this, 104 | ])); 105 | } 106 | 107 | public function getView(): string 108 | { 109 | return $this->view; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Concerns/BelongsToLivewire.php: -------------------------------------------------------------------------------- 1 | livewire = $livewire; 14 | 15 | return $this; 16 | } 17 | 18 | public function getLivewire(): WizardForm 19 | { 20 | return $this->livewire; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Concerns/HasHooks.php: -------------------------------------------------------------------------------- 1 | {$hook}(...$args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Concerns/HasState.php: -------------------------------------------------------------------------------- 1 | state; 12 | } 13 | 14 | public function setState(array $state): static 15 | { 16 | $this->state = $state; 17 | return $this; 18 | } 19 | 20 | public function mergeState(array $state): static 21 | { 22 | $this->state = array_merge($this->state, $state); 23 | return $this; 24 | } 25 | 26 | public function putState($key, $value = null, $default = null): static 27 | { 28 | data_set($this->state, $key, $value, $default); 29 | return $this; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Concerns/HasSteps.php: -------------------------------------------------------------------------------- 1 | activeStep == $step; 17 | } 18 | 19 | public function stepIsGreaterOrEqualThan($step): bool 20 | { 21 | return $this->activeStep >= $step; 22 | } 23 | 24 | public function stepIsLessOrEqualThan($step): bool 25 | { 26 | return $this->activeStep <= $step; 27 | } 28 | 29 | public function goToNextStep($step = null): void 30 | { 31 | $this->setStep($this->nextStep($step)); 32 | } 33 | 34 | public function setStep($step): void 35 | { 36 | $this->callHook('beforeSetStep', $this->activeStep, $step); 37 | 38 | if ($this->hasPrevStep($step)) { 39 | $this->stepsValidation($this->prevStep($step)); 40 | } 41 | 42 | $this->getCurrentStep()->callHook('onStepOut', $this->activeStep); 43 | 44 | $this->activeStep = $step; 45 | 46 | $this->getCurrentStep()->callHook('onStepIn', $step); 47 | 48 | $this->callHook('afterSetStep', $this->activeStep, $step); 49 | } 50 | 51 | public function hasPrevStep($step = null): bool 52 | { 53 | $step ??= $this->activeStep; 54 | return Arr::has($this->steps(), (int) $step - 1); 55 | } 56 | 57 | public function prevStep($step = null): int 58 | { 59 | $step ??= $this->activeStep; 60 | return $this->hasPrevStep($step) ? $step - 1 : $step; 61 | } 62 | 63 | public function nextStep($step = null): int 64 | { 65 | $step ??= $this->activeStep; 66 | return $this->hasNextStep() ? $step + 1 : $step; 67 | } 68 | 69 | public function hasNextStep($step = null): bool 70 | { 71 | $step ??= $this->activeStep; 72 | return Arr::has($this->steps(), $step + 1); 73 | } 74 | 75 | public function stepIsGreaterThan($step): bool 76 | { 77 | return $this->activeStep > $step; 78 | } 79 | 80 | public function totalSteps(): int 81 | { 82 | return count($this->steps()); 83 | } 84 | 85 | public function goToPrevStep($step = null): void 86 | { 87 | $this->setStep($this->prevStep($step)); 88 | } 89 | 90 | public function stepIsLessThan($step): bool 91 | { 92 | return $this->activeStep < $step; 93 | } 94 | 95 | protected function queryStringHasSteps() 96 | { 97 | if ($this->saveStepState) { 98 | return ['activeStep']; 99 | } 100 | 101 | return []; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Contracts/WizardForm.php: -------------------------------------------------------------------------------- 1 | name('livewire-wizard') 14 | ->hasConfigFile() 15 | ->hasViews(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/WizardComponent.php: -------------------------------------------------------------------------------- 1 | callHook('beforeResetForm'); 31 | 32 | $this->setStep(array_key_first($this->steps())); 33 | $this->mount(); 34 | 35 | $this->callHook('afterResetForm'); 36 | } 37 | 38 | public function steps(): array 39 | { 40 | if (property_exists($this, 'steps')) { 41 | return $this->steps; 42 | } 43 | 44 | return []; 45 | } 46 | 47 | public function mount() 48 | { 49 | $this->callHook('beforeMount', ...func_get_args()); 50 | 51 | if (method_exists($this, 'model')) { 52 | $this->model = $this->model(); 53 | } 54 | 55 | $this->stepClasses(function (Step $step) { 56 | 57 | if (method_exists($this, 'model')) { 58 | $step->setModel($this->model); 59 | } 60 | 61 | if (method_exists($step, 'mount')) { 62 | $step->mount(); 63 | } 64 | 65 | if ($step->getOrder() < $this->activeStep && !$step->isValid()) { 66 | $this->setStep($step->getOrder()); 67 | } 68 | }); 69 | 70 | $this->callHook('afterMount', ...func_get_args()); 71 | } 72 | 73 | protected function stepClasses(null|Closure $callback = null): array 74 | { 75 | 76 | if (filled($this->cachedSteps)) { 77 | return collect($this->cachedSteps) 78 | ->each(fn(Step $step, $index) => value($callback, $step, $index)) 79 | ->toArray(); 80 | } 81 | 82 | if (filled($this->steps())) { 83 | $this->cachedSteps = collect($this->steps()) 84 | ->map(function ($step, $index) use ($callback) { 85 | if (class_exists($step) && is_subclass_of($step, Step::class)) { 86 | $stepInstance = $step::make($this); 87 | 88 | if (is_null($stepInstance->getOrder())) { 89 | $stepInstance->setOrder($index); 90 | } 91 | 92 | return $stepInstance; 93 | } 94 | return null; 95 | }) 96 | ->filter() 97 | ->sortBy('order') 98 | ->values() 99 | ->toArray(); 100 | 101 | if ($callback instanceof Closure) { 102 | return $this->stepClasses($callback); 103 | } 104 | } 105 | 106 | return $this->cachedSteps; 107 | } 108 | 109 | public function getModel(): ?Model 110 | { 111 | return $this->model; 112 | } 113 | 114 | public function updated($name, $value): void 115 | { 116 | $this->callHooksStep('updated', $name, $value); 117 | } 118 | 119 | private function callHooksStep($hook, $name, $value): void 120 | { 121 | $stepInstance = $this->getCurrentStep(); 122 | $name = Str::of($name); 123 | 124 | $propertyName = $name->studly()->before('.'); 125 | $keyAfterFirstDot = $name->contains('.') ? $name->after('.')->__toString() : null; 126 | $keyAfterLastDot = $name->contains('.') ? $name->afterLast('.')->__toString() : null; 127 | 128 | $beforeMethod = $hook . $propertyName; 129 | 130 | $beforeNestedMethod = $name->contains('.') 131 | ? $hook . $name->replace('.', '_')->studly() 132 | : false; 133 | 134 | $stepInstance->callHook($beforeMethod, $value, $keyAfterFirstDot); 135 | 136 | if ($beforeNestedMethod) { 137 | $stepInstance->callHook($beforeNestedMethod, $value, $keyAfterLastDot); 138 | } 139 | } 140 | 141 | public function getCurrentStep(): ?Step 142 | { 143 | return $this->getStepInstance($this->activeStep); 144 | } 145 | 146 | public function getStepInstance($step): ?Step 147 | { 148 | if (($stepInstance = data_get($this->stepClasses(), $step)) && !$stepInstance instanceof Step) { 149 | throw new Exception(get_class($stepInstance) . ' must bee ' . Step::class . ' instance'); 150 | } 151 | 152 | return $stepInstance; 153 | } 154 | 155 | public function updating($name, $value): void 156 | { 157 | $this->callHooksStep('updating', $name, $value); 158 | } 159 | 160 | public function save(): void 161 | { 162 | $this->callHook('beforeValidate'); 163 | 164 | $this->stepsValidation(); 165 | 166 | $this->callHook('afterValidate'); 167 | 168 | $state = $this->mutateStateBeforeSave($this->getState()); 169 | 170 | $this->callHook('beforeSave'); 171 | 172 | $this->stepClasses(function (Step $stepInstance) use ($state) { 173 | if (method_exists($stepInstance, 'save')) { 174 | $stepInstance->save($state); 175 | } 176 | }); 177 | 178 | $this->callHook('afterSave'); 179 | } 180 | 181 | protected function stepsValidation($step = null): void 182 | { 183 | [$rules, $messages, $attributes] = [[], [], []]; 184 | $step = $step ?? max(array_keys($this->steps())); 185 | 186 | $this->stepClasses(function (Step $stepInstance) use ($step, &$rules, &$messages, &$attributes) { 187 | 188 | if (method_exists($stepInstance, 'validate') && $stepInstance->getOrder() <= $step) { 189 | $stepValidate = $stepInstance->validate(); 190 | $stepInstance->validationFails = !$stepInstance->isValid(); 191 | 192 | $rules = array_merge($rules, $stepValidate[0] ?? []); 193 | $messages = array_merge($messages, $stepValidate[1] ?? []); 194 | $attributes = array_merge($attributes, $stepValidate[2] ?? []); 195 | } 196 | }); 197 | 198 | if (filled($rules)) { 199 | $this->validate($rules, $messages, $attributes); 200 | } 201 | } 202 | 203 | public function mutateStateBeforeSave(array $state = []): array 204 | { 205 | return $state; 206 | } 207 | 208 | public function render(): View 209 | { 210 | return view('livewire-wizard::wizard', [ 211 | 'stepInstances' => $this->stepClasses(), 212 | ]); 213 | } 214 | } 215 | --------------------------------------------------------------------------------