Livewire Form Wizard with Validation Support

Tailwind CSS Livewire

Demo

1 About You
2 Your Requirements
3 Finishing Up

This is a pro component. Get Livewiredemos pro access to view / download the component code.

Component Class


<?php

namespace App\Http\Livewire\Pro;

use Livewire\Component;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Validator;

class LivewireFormWizard extends Component
{
    public $steps = [
        'Step One',
        'Step Two',
        'Step Three'
    ]; 


    public $currentStep = 1;
    public $totalSteps;
    public $uuid;
    protected $queryString = ['uuid'];

    public $storeValuesInSession = false;

    public function render()
    {
        
        return view('livewire.pro.livewire-form-wizard');
    }

    protected $rules = [
       
    ];

    public function mount(){
        $this->getValuesFromSession();
        $this->totalSteps = count($this->steps);
    }

    public function next(){
        if($this->currentStep < $this->totalSteps){
            if(isset($this->rules[$this->currentStep - 1])){
                $this->validate($this->rules[$this->currentStep - 1]);
            }
            $this->currentStep++;
            $this->storeValuesInSession();
            $this->emit('moveNext');
        }
    }

    public function submit(){
        $this->validateMultipleSteps(1, $this->totalSteps);  //Validate all steps on final submit
        $this->emit('formSubmit');
    }

    public function previous(){
        if($this->currentStep > 1){
            $this->currentStep--;
            $this->storeValuesInSession();
            $this->emit('movePrevious');
        }
    }

    public function switchStep($step){
        if($step > $this->currentStep){
            $this->validateMultipleSteps($this->currentStep, $step);  //Validate steps from current till the step user wants to swtich to
        }
        $this->storeValuesInSession();
        $this->currentStep = $step;
        $this->emit('switchStep');
        
    }

    protected function validateMultipleSteps($fromStep, $toStep){
        for($i=$fromStep - 1; $i< $toStep - 1; $i++){
            if(isset($this->rules[$this->currentStep - 1])){
                $data = $this->getDataForValidation($this->rules[$i]);
                $validated = Validator::make($data, $this->rules[$i]);
                if($validated->fails()){
                    $this->currentStep = $i + 1;
                    $this->storeValuesInSession();
                    $this->validate($this->rules[$i]);
                    return;
                }
            }
        }
    }

    protected function storeValuesInSession(){
       if($this->storeValuesInSession){
        session([$this->uuid => get_object_vars($this)]);
       }
    }

    protected function getValuesFromSession(){
        if($this->storeValuesInSession){
            if(!isset($this->uuid) || !Session::has($this->uuid)){
                $this->uuid = (string) Str::uuid();
                $this->storeValuesInSession();
            }else{
                //Set values in object from session
                $values = session($this->uuid);
                foreach($values as $key => $value){
                    $this->$key = $value;
                }
            }
        }
    }

}

Component View File


<div>
    <div id="stepIndicator" class="flex justify-between my-2">
       @foreach($steps as $key => $step)
         <div wire:click="switchStep({{$key+1}})" class="rounded p-2 grow text-center mx-1 cursor-pointer  {{$currentStep == $key+1 ? 'bg-purple-300' : 'bg-purple-100'}}">
            <span  class="rounded-full bg-gray-300 inline-block w-6 h-6 mx-2">{{$key+1}}</span> {{$step}}
         </div>
       @endforeach
    </div>
    <div id="stepContent" class="border rounded p-3 relative">
       <div class="max-w-screen-md mx-auto">
         @if($currentStep == 1)
         <div class="my-3">
            Step 1 Form Fields Goes Here
         </div>
         @endif
         @if($currentStep == 2)
         <div class="my-3">
               Step 2 Form Fields Goes Here
         </div>
         @endif
         @if($currentStep == 3)
         <div class="my-3">
            Step 3 Form Fields Goes Here
         </div>
         @endif
       </div>
       <div wire:loading.flex wire:target="next, previous, submit, switchStep" class="absolute w-full h-full bg-gray-200 bg-opacity-50 top-0 left-0 flex items-center justify-center">
         <svg role="status" class="inline w-8 h-8 mr-2 text-gray-300 animate-spin dark:text-gray-600 fill-purple-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
            <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
         </svg>
       </div>
    </div>
    <div id="stepButtons" class="text-center my-2">
        <button class="p-2 rounded {{$currentStep == 1 ? 'bg-gray-200' : 'bg-purple-300'}}"  wire:click="previous">Previous</button>
        @if($currentStep < $totalSteps)
        <button class="p-2 rounded {{$currentStep == $totalSteps ? 'bg-gray-200' : 'bg-purple-300'}}" wire:click="next">Next</button>
        @endif
        @if($currentStep == $totalSteps)
        <button class="p-2 rounded bg-purple-400" wire:click="submit">Submit</button>
        @endif
    </div>
</div>

Usage

Extend the LivewireFormWizard component to create your a form wizard with your custom steps and form fields


<?php

namespace App\Http\Livewire\Pro;

use Livewire\Component;

class FormWizardDemo extends LivewireFormWizard
{
    public $fullname;
    public $email;
    public $organization;
    public $notificationMethod = [];
    public $interestedComponents = [];
    public $refer;
    public $message;

    //Override
    public $steps = [
        'About You',
        'Your Requirements',
        'Finishing Up'
    ]; 
    public $storeValuesInSession = true;


    public function render()
    {
        return view('livewire.pro.form-wizard-demo');
    }

    protected $rules = [
        [
            'fullname' => 'required',
            'email' => 'required|email',
            'organization' => 'required',
        ],
        [
            'notificationMethod' => 'required',
            'interestedComponents' => 'required',
        ],
        [
            'refer' => 'required'
        ]
    ];
}


<div>
    <div id="stepIndicator" class="flex justify-between my-2">
       @foreach($steps as $key => $step)
         <div wire:click="switchStep({{$key+1}})" class="rounded p-2 grow text-center mx-1 cursor-pointer  {{$currentStep == $key+1 ? 'bg-purple-300' : 'bg-purple-100'}}">
            <span  class="rounded-full bg-gray-300 inline-block w-6 h-6 mx-2">{{$key+1}}</span> {{$step}}
         </div>
       @endforeach
    </div>
    <div id="stepContent" class="border rounded p-3 relative">
       <div class="max-w-screen-md mx-auto">
        @if($currentStep == 1)
        <div>
           <div class="my-3">
              <label for="fullname" class="block text-sm font-medium text-gray-700">Full Name</label>
              <div class="mt-1">
                  <input wire:model.lazy="fullname" type="text" name="fullname" id="fullname" class="shadow-sm p-2 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border border-gray-300 rounded-md" placeholder="Your Full Name">
                  @error('fullname')
                  <span class="mt-1 text-red-600">{{$message}}</span>
                  @enderror
              </div>
           </div>
           <div class="my-3">
              <label for="email" class="block text-sm font-medium text-gray-700">Email</label>
              <div class="mt-1">
                  <input wire:model.lazy="email" type="email" name="email" id="email" class="shadow-sm p-2 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border border-gray-300 rounded-md" placeholder="you@example.com">
                  @error('email')
                  <span class="mt-1 text-red-600">{{$message}}</span>
                  @enderror
              </div>
           </div>
           <div class="my-3">
              <label for="organization" class="block text-sm font-medium text-gray-700">Organization</label>
              <div class="mt-1">
                  <input wire:model.lazy="organization" type="text" name="organization" id="organization" class="shadow-sm p-2 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border border-gray-300 rounded-md" placeholder="Your organization">
                  @error('organization')
                  <span class="mt-1 text-red-600">{{$message}}</span>
                  @enderror
              </div>
           </div>
        </div>
        @endif
       @if($currentStep == 2)
       <div class="my-3">
       <div>
         <label class="text-base font-medium text-gray-900">Notifications</label>
         <p class="text-sm leading-5 text-gray-500">How do you prefer to receive notifications?</p>
         <fieldset>
         <legend class="sr-only">Notification method</legend>
           <div class="space-y-2">
            <div class="flex items-center">
               <input id="email" wire:model="notificationMethod" name="notificationMethod" value="email" type="radio" checked class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300">
               <label for="email" class="ml-3 block text-sm font-medium text-gray-700"> Email </label>
            </div>

             <div class="flex items-center">
                 <input id="sms" wire:model="notificationMethod" name="notificationMethod" value="sms" type="radio" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300">
                 <label for="sms" class="ml-3 block text-sm font-medium text-gray-700"> Phone (SMS) </label>
             </div>

             <div class="flex items-center">
                  <input id="push" wire:model="notificationMethod" name="notificationMethod" value="push" type="radio" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300">
                  <label for="push" class="ml-3 block text-sm font-medium text-gray-700"> Push notification </label>
             </div>
           </div>
        </fieldset>
        @error('notificationMethod')
            <span class="mt-1 text-red-600">{{$message}}</span>
         @enderror
        </div>
        </div>
        <div class="my-3">
       <div>
         <label class="text-base font-medium text-gray-900">Interested In</label>
         <p class="text-sm leading-5 text-gray-500">Please select the components you are interested in?</p>
         <fieldset class="mt-4">
         <legend class="sr-only">Interested Tools</legend>
           <div class="space-y-2">
            <div class="flex items-center">
               <input wire:model="interestedComponents" id="livewire" value="livewire" name="interestedComponents" type="checkbox" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300">
               <label for="livewire" class="ml-3 block text-sm font-medium text-gray-700"> Livewire Components </label>
            </div>

             <div class="flex items-center">
                 <input wire:model="interestedComponents" id="alpinejs" value="alpinejs" name="interestedComponents" type="checkbox" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300">
                 <label for="alpinejs" class="ml-3 block text-sm font-medium text-gray-700"> AlpineJS Components </label>
             </div>

             <div class="flex items-center">
                  <input wire:model="interestedComponents" id="tailwindcss" value="tailwindcss" name="interestedComponents" type="checkbox" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300">
                  <label for="tailwindcss" class="ml-3 block text-sm font-medium text-gray-700"> TailwindCSS Components </label>
             </div>
           </div>
        </fieldset>
        @error('interestedComponents')
            <span class="mt-1 text-red-600">{{$message}}</span>
         @enderror
        </div>
        </div>
        @endif
       @if($currentStep == 3)
       <div>
       <div>
           <div class="my-3">
              <label for="refer" class="block text-sm font-medium text-gray-700">How did you hear about us?</label>
              <div class="mt-1">
              <select wire:model="refer" id="refer" name="refer" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md">
                 <option>Select an option</option>
                 <option value="friend">Friend</option>
                 <option value="google">Google</option>
                 <option value="twitter">Twitter</option>
                 <option value="youtube">Youtube</option>
               </select>
                  @error('refer')
                      <span class="mt-1 text-red-600">{{$message}}</span>
                  @enderror
              </div>
           </div>
           <div class="my-3">
              <label for="email" class="block text-sm font-medium text-gray-700">Any Custom Requirements / Message</label>
              <div class="mt-1">
                  <textarea name="message" class="border border-gray-300 rounded w-full p-2">

                  </textarea>
              </div>
           </div>
        </div>
       </div>
       @endif
       </div>
       <div>
         <div wire:loading.flex wire:target="next, previous, submit, switchStep" class="absolute w-full h-full bg-gray-200 bg-opacity-50 top-0 left-0 flex items-center justify-center">
            <svg role="status" class="inline w-8 h-8 mr-2 text-gray-300 animate-spin dark:text-gray-600 fill-purple-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
               <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
               <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
            </svg>
         </div>
      </div>
    </div>
    <div id="stepButtons" class="text-center my-2">
        <button class="p-2 rounded {{$currentStep == 1 ? 'bg-gray-200' : 'bg-purple-300'}}"  wire:click="previous">Previous</button>
        @if($currentStep < $totalSteps)
        <button class="p-2 rounded {{$currentStep == $totalSteps ? 'bg-gray-200' : 'bg-purple-300'}}" wire:click="next">Next</button>
        @endif
        @if($currentStep == $totalSteps)
        <button class="p-2 rounded bg-purple-400" wire:click="submit">Submit</button>
        @endif
    </div>
</div>