DEV Community

MongoDB Guests
MongoDB Guests

Posted on

Building a Multi-Step Form with Laravel, Livewire, and MongoDB

This article was wriiten by Moses Anumadu.

In modern-day applications, there is often a need to collect user data through forms. When many details are required from users, the form can be very long and look messy. This often leads to a bad user experience and a high bounce rate from the application. A simple solution to this is creating multi-step forms.
This tutorial will build a multi-step form using MongoDB, Laravel, and Laravel Livewire.
For this tutorial, we are using MongoDB as our database because it stores data in nested form, document-based model, and has a flexible schema. MongoDB does not need many tables and relationships like a typical relational database would. Also, it does not require creating migrations. If you know nothing about MongoDB, you can read our Getting Started docs for more.

Livewire, on the other hand, is a full-stack Laravel framework that enables developers to build dynamic interfaces easily without the need to write extensive JavaScript code.

Livewire is easy to use and follows Laravel's syntax. Check out the Laravel documentation to get started!

With that said, let's get started.

Prerequisites

To follow along with this tutorial, the following are required:

Environment setup

Before proceeding, ensure that you have the MongoDB PHP Driver installed in the development environment so that MongoDB can work effectively. To verify, run the command below:

php -i | grep mongo
Enter fullscreen mode Exit fullscreen mode

The expected output should look similar to the image below:

A screen shot of a terminal with the expected output that show a user has MongoDB PHP Driver installed correctly.

If you have a different output, it means you probably do not have the MongoDB PHP Driver installed. To install it, visit the MongoDB extension for detailed instructions on how to install it.

Also, ensure that you have PHP and Composer installed before proceeding.

Project setup

Let's get started building our multi-step form by creating a fresh Laravel project. We can do this using the command below:

composer create-project laravel/laravel multi_step_form_tutorial
Enter fullscreen mode Exit fullscreen mode

After creating the Laravel project, we need to configure the application to work with MongoDB. Laravel does not come configured to work with MongoDB out of the box. We must install and configure the Laravel-MongoDB package to get it to work. So, let's get started by installing the Laravel-mongodb package using the command below:

composer require mongodb/laravel-mongodb
Enter fullscreen mode Exit fullscreen mode

After successfully installing the Laravel-MongoDB package, we need to adjust some of the configuration in the config/database.php file to get it to work. You can do that by adding the code below:

'mongodb' => [
'driver' => 'mongodb',
'dsn' => env('MONGODB_URI'),
'database' => 'YOUR_DATABASE_NAME',
],
Enter fullscreen mode Exit fullscreen mode

Let's take a moment to explain. The dsn value is obtained from the .env file. In your .env file, create a value for MONGODB_URI and set it to the value of your MongoDB Atlas connection string, like below:

MONGODB_URI="<<MONGODB_ATLAS_CONNECTION_STRING>>"
DB_CONNECTION=mongodb
Enter fullscreen mode Exit fullscreen mode

Set up Livewire

Let's proceed to install and configure Livewire to work in our application. We first need to install Livewire using the command:

composer require livewire/livewire
Enter fullscreen mode Exit fullscreen mode

After installing Livewire, we need to import it into the app. To keep things organized, let's create a layout file and make it a Blade component file. If Blade templates sound new to you, check out the Laravel documentation to get started. You can also get a deeper dive into Blade custom directives. With that said, let's generate our Blade template using the artisan command below:

php artisan make:component layouts/App
Enter fullscreen mode Exit fullscreen mode

This will generate two files: app/View/Components/layouts/App.php and resources/views/components/layouts/app.blade.php. Open resources/views/components/layouts/app.blade.php and replace the content of the page with the code below:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Multi-Step Form</title>
@vite(['resources/css/app.css', 'resources/js/app.js']) {{-- If using Vite --}}
@livewireStyles
</head>
<body class="bg-gray-100">
<div class="container mx-auto mt-10">
{{ $slot ?? '' }}
</div>
@livewireScripts
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

From the code above, we added the HTML layout for the layout file. We also imported Livewire using @livewireStyles and @livewireScripts. This makes Livewire available on any page with this layout.
Before we create the Livewire multi-form component, let's ensure that the database configuration works correctly. To do this, we need to create a route in routes/web.php to ping our MongoDB Atlas cluster and ensure things are set up correctly. Navigate to routes/web.php and add the following code snippet below the existing code on the page:

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

Route::get('/ping', function (Request $request) {
$connection = DB::connection('mongodb');
try {
$connection->command(['ping' => 1]);
$msg = 'MongoDB is accessible!';
} catch (Exception $e) {
$msg = 'You are not connected to MongoDB. Error: ' . $e->getMessage();
}
return ['msg' => $msg];
});
Enter fullscreen mode Exit fullscreen mode

Now, start the application using:

php artisan serve
Enter fullscreen mode Exit fullscreen mode

This will start the application with the IP address 127.0.0.1:8000. If the 8000 port is not available, Laravel will automatically switch to an available port.
In a different terminal window, run the following to install and build the Node.js dependencies in the application.

Npm install && Npm run dev
Enter fullscreen mode Exit fullscreen mode

Let's test. From your browser, navigate to the newly created route, 127.0.0.1:8000/ping. If everything was done right, your screen should look just like the image below:

Image of a browser tab with a JSON response saying

Building the Livewire component

Before building the Livewire component, let's talk a little about how our form will work. We are building a multi-step form with three (3) steps. The first step will contain the basic user information, like name and email. The second step will contain address details like street, city, or state, and the third will contain pieces of information like checkboxes and dropdowns for gender and marital status.

The form will be programmed to validate data for each step, save the validated data, and have a back, next, and submit button. Information will be saved for each validated step in our database.

MongoDB schema is very flexible and often preferred when handling dynamic data. Its schematic approach to saving data makes it a good approach for our form here. With MongoDB, we can easily add new fields to the form and don’t need to add extra columns to a migration file. Also, when working with MongoDB in Laravel, we don't need to create migration files. New fields can be added on the fly.

With that said, let's create our Livewire component using the command below:

php artisan livewire:make MultiStepForm
Enter fullscreen mode Exit fullscreen mode

After running the command above, it will create a Livewire component with two files: app/Livewire/MultiStepForm.php and resources/views/livewire/multi-step-form.blade.php. app/Livewire/MultiStepForm.php will contain all the PHP code for the component while resources/views/livewire/multi-step-form.blade.php will contain the HTML side of things.

Let's import our Livewire component into our resources/views/welcome.blade.php file by updating the content of the page with the code below:

<x-layouts.app>
<livewire:multi-step-form />
</x-layouts.app>
Enter fullscreen mode Exit fullscreen mode

In routes/web.php, create a route for our Livewire component by adding the code below.

Route::get('/', function () {
    return view('welcome');
});
Enter fullscreen mode Exit fullscreen mode

With that done, replace the content of resources/views/livewire/multi-step-form.blade.php with the code below:

<div>
    <div class="max-w-4xl mx-auto p-6 bg-white shadow-xl rounded-xl">
        @if (session('success'))
            <div class="mb-4 p-4 bg-green-100 text-green-700 rounded-lg">
                {{ session('success') }}
            </div>
        @endif
        <h1 class="text-4xl">Multi step form with MongoDB</h1>
        <h2 class="text-base font-semibold mb-4">Step {{ $currentStep }} of 3</h2>
        <div>
            @if ($currentStep == 1)
                <div>
                    <label class="block text-sm">Name</label>
                    <input type="text" wire:model="name" class="border rounded-lg p-2 w-full">
                    @error('name') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror

                    <label class="block mt-2 text-sm">Email</label>
                    <input type="email" wire:model="email" class="border rounded-lg p-2 w-full">
                    @error('email') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
                </div>
            @endif

            @if ($currentStep == 2)
                <div>
                    <label class="block text-sm">Address</label>
                    <input type="text" wire:model="address" class="border p-2 w-full">
                    @error('address') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror

                    <label class="block mt-2">City</label>
                    <input type="text" wire:model="city" class="border p-2 w-full">
                    @error('city') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror

                </div>
            @endif

            @if ($currentStep == 3)
                <div>
                    <label class="block">Gender</label>
                    <select wire:model="gender" class="border p-2 w-full">
                        <option value="male">Male</option>
                        <option value="female">Female</option>
                    </select>
                </div>
            @endif

            <div class="mt-4 flex justify-between">
                @if ($currentStep > 1)
                    <button wire:click="previousStep" class="px-4 py-2 bg-gray-500 text-white rounded">Back</button>
                @endif

                @if ($currentStep < 3)
                    <button wire:click="nextStep" wire:loading.class="opacity-50" class="px-4 py-2 bg-blue-500 text-white rounded">
                        <span wire:loading.remove>Next</span>
                        <span wire:loading>
                            Loading..
                        </span>
                    </button>
                @else
                    <button wire:click="nextStep" wire:loading.class="opacity-50" class="px-4 py-2 bg-green-500 text-white rounded">
                        <span wire:loading.remove>Save</span>
                        <span wire:loading>
                            Loading..
                        </span>
                    </button>
                @endif
            </div>
        </div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Let's break down the code above. It contains the HTML and TailwindCSS for our multi-step form. The form fields contain wire:model which binds them to elements that will be declared in the Livewire component. The form is wrapped in a centered container div with Tailwind classes for spacing, background color, and a subtle shadow.

In the code, we use the ($currentStep) variable to update the current step of the form as the user fills the form.

 @if (session('success'))
         <div class="mb-4 p-4 bg-green-100 text-green-700 rounded-lg">
              {{ session('success') }}
          </div>
   @endif
Enter fullscreen mode Exit fullscreen mode

Check if a success message exists in the session. If this returns true, it’s displayed with a green background and a lighter green text.

We used a conditional statement and the value of $currentStep to identify the current step and display the relevant information for that step:

@if ($currentStep == 1)
 @enderror
Enter fullscreen mode Exit fullscreen mode

This shows each step based on the value of $currentStep. In step one, it displays fields to collect names and emails. These forms’ input are bound to $name and $email in app/Livewire/MultiStepForm.php using wire:model. We repeated the process for steps two and three. We also handle errors in the form using the @error() Blade directive.

We used a conditional statement to display the back button for the second and third steps, like the code below:

@if ($currentStep > 1)
                    <button wire:click="previousStep" class="px-4 py-2 bg-gray-500 text-white rounded">Back</button>
                @endif
Enter fullscreen mode Exit fullscreen mode

We did the same for the Next button to proceed to the next step. We added a save button in the final stage to save the value in step three. To spice it up, we added wire:loading buttons to display a loading place holder when the form is loading.

Handling form data with MongoDB

The MongoDB PHP driver makes it very easy to work with MongoDB in Laravel. It enables us to interact with MongoDB using Eloquent the same way we would in a relational database situation (MySQL or PostgreSQL). To get started, let's create a model form using the command below:

php artisan make:model MultiStepForm
Enter fullscreen mode Exit fullscreen mode

The command above will create a app\Models\MultiStepForm.php file. Open the file and update the content with the following:

<?php
namespace App\Models;
use MongoDB\Laravel\Eloquent\Model;
class MultiStepForm extends Model
{
protected $connection = 'mongodb';
protected $table = 'multi_step_form';

protected $fillable = [
'name', 'email', 'address', 'city', 'gender',
];
}
Enter fullscreen mode Exit fullscreen mode

This is a typical Laravel model class. We imported MongoDB Eloquent using use MongoDB\Laravel\Eloquent\Mode. We connected the model to MongoDB using protected $connection = 'mongodb' and created a database table named multi_step_form using the protected $table = 'multi_step_form'.

Handling the form in the Livewire class

Let's handle the rest of the functionality of our form using Livewire.

Navigate to app/Livewire/MultiStepForm.php and replace the content of the page with the code below:

<?php
namespace App\Livewire;
use Livewire\Component;
use Illuminate\Support\Facades\DB;
use App\Models\MultiStepForm as MultiStepFormDB;
class MultiStepForm extends Component
{
    public $currentStep = 1;
    public $totalSteps = 3;
    public $name;
    public $email;
    public $address;
    public $city;
    public $gender;
    public function nextStep()
    {
        $this->validateStep();
        if($this->currentStep < $this->totalSteps){
            $this->currentStep++;
        }
    }
    public function previousStep()
    {
        $this->currentStep--;
    }
    public function validateStep()
    {
        if($this->currentStep === 1){
            $rules = [
                'name' => 'required|string',
                'email' => 'required|email',
            ];
            $this->validate($rules);
            $this->saveProgress($this->email, ['name' => $this->name], ['email' => $this->email]);
            session()->flash('success', 'Step '.$this->currentStep.' saved');
        }elseif($this->currentStep === 2){
            $rules = [
                'address' => 'required|string',
                'city' => 'required|string',
            ];
            $this->validate($rules);
            $this->saveProgress($this->email, ['address' => $this->address], ['city' => $this->city]);
            session()->flash('success', 'Step '.$this->currentStep.' saved');
        }elseif($this->currentStep === 3){
            $rules = [
                'gender' => 'required|string',
            ];
            $this->validate($rules);
            $this->saveProgress($this->email, ['gender' => $this->gender]);
            session()->flash('success', 'Step '.$this->currentStep.' saved');
        }
    }
    public function saveProgress($email, ...$formFields)
    {
        $data = [];
        foreach($formFields as $value){
            if (is_array($formFields)) {
                $data = array_merge($data, $value);
            }
        }
        try{
            MultiStepFormDB::updateOrCreate(['email' => $email], $data);
        }catch(\Exception $e){
            \Log::error('Error saving MultiStepForm: ' . $e->getMessage());
            session()->flash('error', 'There was an error saving your data. Please try again.');
        }
    }
    public function render()
    {
        return view('livewire.multi-step-form');
    }
}
Enter fullscreen mode Exit fullscreen mode

The logic of the form we created in resources/views/livewire/multi-step-form.blade.php is done in app\Models\MultiStepForm.php. Let's break down the code above for easier understanding.
To keep track of the steps, we declared two variables for this purpose. The $currentStep keeps track of the user's current step and has a default value of 1. The $totalSteps variable defines how many steps the form has. It has a default value of 3, indicating that our form has three steps in total.
We declared other public properties to hold form data, like name and email, address and city, and finally gender.
These are Livewire properties. They automatically update in real time as the user types and are connected to the form using wire:model which we mentioned earlier.

nextStep(): This method validates the form input by calling the $this->validateStep() method. It displays an error message if the form fields are not fielded correctly. If everything is done right, it moves the user to the next step. It also flashes a success message to show progress.

previousStep(): This method moves the user one step back in the form by decreasing the value of $this->currentStep less by one.

$this->validateStep(): This method handles form validation and updates the database after each step to avoid data loss. It checks for the current step, validates the data required for the step, and updates the database—in step one, validated for name and email; in step two, validated for address and city; and finally, in step three, validated for gender. If there is an error, we display it. This error is displayed as an error message in the blade.php file.
Each time the user moves to the next step, their data is saved using the $this->saveProgress().

$this->saveProgress(): This is a custom method created for creating or updating data in our MongoDB database and to prevent us from repeating ourselves.

 public function saveProgress($email, ...$formFields)
    {
        $data = [];
        foreach($formFields as $value){
            if (is_array($formFields)) {
                $data = array_merge($data, $value);
            }
        }
        try{
            MultiStepFormDB::updateOrCreate(['email' => $email], $data);
        }catch(\Exception $e){
            \Log::error('Error saving MultiStepForm: ' . $e->getMessage());
            session()->flash('error', 'There was an error saving your data. Please try again.');
        }
    }
Enter fullscreen mode Exit fullscreen mode

It accepts the $email of the user. The $email is used as a unique identifier to update a record that already exists or create a new record that does not exist.
I also accept another argument, ...$formFields. This enables us to pass in unlimited numbers of arrays to the method. We looped through and assigned the correct value to $data which is passed to our MultiStepFormDB::updateOrCreate() method to create the record.
The render() is shipped with Livewire. It loads the livewire.multi-step-form Blade view, which contains the form.

Testing

At this point, we can test. Ensure that your Laravel application is still served and npm is still running. If not still served, you can reserve it using the command below:

php artisan serve
Enter fullscreen mode Exit fullscreen mode

From a different window:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Navigate to the local host URL. In my case, it is http://127.0.0.1:8000/. You should see a screen just like mine:

A GIF representing steps to test theapplication

Fill out the form and ensure that everything works as expected.

Conclusion

Good job, if you followed along up until now. We have come to the end of the tutorial. Let's take a minute to recap. In this tutorial, we:

  • Built a functional, multi-step form using Laravel and Livewire.
  • Utilized MongoDB’s schema flexibility to store nested form data effectively.
  • Leveraged the MongoDB PHP Driver for seamless data interaction.

If you like a little challenge, you can try adding user authentication to your form, and integrating email notifications using Laravel and MongoDB triggers for incomplete forms.

Find the project on GitHub. Feel free to clone it, sign up for MongoDB Atlas, and customize it to your specific needs. For more support, join the MongoDB Developer Community.

Heroku

Built for developers, by developers.

Whether you're building a simple prototype or a business-critical product, Heroku's fully-managed platform gives you the simplest path to delivering apps quickly — using the tools and languages you already love!

Learn More

Top comments (0)

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

👋 Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay