Laravel livewire crud tutorial using Tailwind modal

In this article, we will see how we can use Livewire to implement AJAX Pagination, AJAX Sorting, AJAX Searching, AJAX Filtering, Delete using Confirmation Modal, Add using Modal and Edit using Modal. We will be using Jestream Livewire Stack, which also uses Tailwind CSS to implement all this functionality. We will be using Modals and other Components provided by Jetstream. So lets see a step by step process to implement this.

If you are interested please checkout our Livewire Package tall-crud-generator which automatically generates all the Livewire Code based on your Model and its relationships.

Step 1- Laravel jetstream installation

You can install Laravel using the following command:

laravel new laravel_project --jet

Then when prompted, choose the Livewire Stack. After the installation, we then need to run following commands:

Go inside the Newly Created Project Directory.

cd laravel_project

Install composer dependencies using

composer install

Install npm packages using

npm install

Finally run the Migration using below command

php artisan migrate

At this stage you can start the Server using the below command

php artisan serve

Now you can open the project at http://127.0.0.1:8000. You will see the Laravel Homepage. Go ahead and register the User.

Step 2- Setting Up Model

We then need to setup the Model. We can do so using below command. This will create the Model, Migration File as well as Factory File

php artisan make:model Item -mf

We will set up the Migration Up method as below:

    public function up()
    {
        Schema::create('items', function (Blueprint $table) {
            $table->id();
            $table->integer('user_id')->index();
            $table->string('name');
            $table->float('price', 8 , 2);
            $table->boolean('status');
            $table->timestamps();
        });
    }

We will then again run the migration using php artisan migrate. This will create the table in the Database. We can then setup the Factory File like below:

    public function definition()
    {
        return [
            //
            'user_id' => User::factory(),
            'name' => $this->faker->word,
            'price' => $this->faker->randomNumber(2),
            'status' => $this->faker->boolean()
        ];
    }

We can then go to tinker and insert some random records for testing using the below command

\App\Models\Item::factory()->count(40)->create(['user_id' => 1]);

This will create the 40 records in the Database, all linked to the User with id 1 with which we registered in the above step.

We can then setup our Item Model and define the Relationship as well as a Local Scope which we will use. Our Item Model will look like below:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Item extends Model
{
    use HasFactory;
    protected $fillable = ['name', 'price', 'status'];

    public function user() 
    {
        return $this->belongsTo(\App\Models\User::class);
    }

    public function scopeActive( $query) 
    {
        return $query->where('status', 1);
    }
}

Step 3 – Setting Up the Route and Menu

We can setup a New Route in our web.php file like below

Route::middleware(['auth:sanctum', 'verified'])->get('/items', function () {
    return view('items');
})->name('items');

We then change the Menu File provided by Jetstream at /resources/views/navigation-dropdown.blade.php

                    <x-jet-nav-link href="{{ route('items') }}" :active="request()->routeIs('items')">
                        {{ __('Items') }}
                    </x-jet-nav-link>

Step 4 – Creating Laravel Livewire component

We will then create our Livewire Component using below command:

php artisan make:livewire items

This will create our Livewire Component File and Livewire View File. We will create our View File for the route that we defined and call the Livewire Component in that view file. We will create this file at resources/views/items.blade.php

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Items') }}
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
                <livewire:items />
            </div>
        </div>
    </div>
</x-app-layout>

Step 5 – Setting the Livewire Component

In our Livewire Component, we will then implement all the functionality for Pagination, Searching, Sorting, Add, Edit and Delete. Our Component will finally look like below:

<?php

namespace App\Http\Livewire;

use Livewire\Component;
use App\Models\Item;
use Livewire\WithPagination;

class Items extends Component
{
    use WithPagination;

    public $active;
    public $q;
    public $sortBy = 'id';
    public $sortAsc = true;
    public $item;

    public $confirmingItemDeletion = false;
    public $confirmingItemAdd = false;

    protected $queryString = [
        'active' => ['except' => false],
        'q' => ['except' => ''],
        'sortBy' => ['except' => 'id'],
        'sortAsc' => ['except' => true],
    ];

    protected $rules = [
        'item.name' => 'required|string|min:4',
        'item.price' => 'required|numeric|between:1,100',
        'item.status' => 'boolean'
    ];

    public function render()
    {
        $items = Item::where('user_id', auth()->user()->id)
            ->when( $this->q, function($query) {
                return $query->where(function( $query) {
                    $query->where('name', 'like', '%'.$this->q . '%')
                        ->orWhere('price', 'like', '%' . $this->q . '%');
                });
            })
            ->when($this->active, function( $query) {
                return $query->active();
            })
            ->orderBy( $this->sortBy, $this->sortAsc ? 'ASC' : 'DESC');

        $items = $items->paginate(10);

        return view('livewire.items', [
            'items' => $items,
        ]);
    }

    public function updatingActive() 
    {
        $this->resetPage();
    }

    public function updatingQ() 
    {
        $this->resetPage();
    }

    public function sortBy( $field) 
    {
        if( $field == $this->sortBy) {
            $this->sortAsc = !$this->sortAsc;
        }
        $this->sortBy = $field;
    }

    public function confirmItemDeletion( $id) 
    {
        $this->confirmingItemDeletion = $id;
    }

    public function deleteItem( Item $item) 
    {
        $item->delete();
        $this->confirmingItemDeletion = false;
        session()->flash('message', 'Item Deleted Successfully');
    }

    public function confirmItemAdd() 
    {
        $this->reset(['item']);
        $this->confirmingItemAdd = true;
    }

    public function confirmItemEdit(Item $item) 
    {
        $this->resetErrorBag();
        $this->item = $item;
        $this->confirmingItemAdd = true;
    }

    public function saveItem() 
    {
        $this->validate();

        if( isset( $this->item->id)) {
            $this->item->save();
            session()->flash('message', 'Item Saved Successfully');
        } else {
            auth()->user()->items()->create([
                'name' => $this->item['name'],
                'price' => $this->item['price'],
                'status' => $this->item['status'] ?? 0
            ]);
            session()->flash('message', 'Item Added Successfully');
        }

        $this->confirmingItemAdd = false;

    }
}

Step 6 – Setting Laravel Livewire View

Our Livewire View File will define our View Layer. It is located at resources/views/livewire/items.blade.php and it’s final Code looks like below:

<div class="p-6 sm:px-20 bg-white border-b border-gray-200">
    @if(session()->has('message'))
    <div class="flex items-center bg-blue-500 text-white text-sm font-bold px-4 py-3 relative" role="alert" x-data="{show: true}" x-show="show">
        <svg class="fill-current w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M12.432 0c1.34 0 2.01.912 2.01 1.957 0 1.305-1.164 2.512-2.679 2.512-1.269 0-2.009-.75-1.974-1.99C9.789 1.436 10.67 0 12.432 0zM8.309 20c-1.058 0-1.833-.652-1.093-3.524l1.214-5.092c.211-.814.246-1.141 0-1.141-.317 0-1.689.562-2.502 1.117l-.528-.88c2.572-2.186 5.531-3.467 6.801-3.467 1.057 0 1.233 1.273.705 3.23l-1.391 5.352c-.246.945-.141 1.271.106 1.271.317 0 1.357-.392 2.379-1.207l.6.814C12.098 19.02 9.365 20 8.309 20z"/></svg>
        <p>{{ session('message') }}</p>
        <span class="absolute top-0 bottom-0 right-0 px-4 py-3" @click="show = false">
            <svg class="fill-current h-6 w-6 text-white" role="button" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><title>Close</title><path d="M14.348 14.849a1.2 1.2 0 0 1-1.697 0L10 11.819l-2.651 3.029a1.2 1.2 0 1 1-1.697-1.697l2.758-3.15-2.759-3.152a1.2 1.2 0 1 1 1.697-1.697L10 8.183l2.651-3.031a1.2 1.2 0 1 1 1.697 1.697l-2.758 3.152 2.758 3.15a1.2 1.2 0 0 1 0 1.698z"/></svg>
        </span>
    </div>
    @endif
    <div class="mt-8 text-2xl flex justify-between">
        <div>Items</div> 
        <div class="mr-2">
            <x-jet-button wire:click="confirmItemAdd" class="bg-blue-500 hover:bg-blue-700">
                Add New Item
            </x-jet-button>
        </div>
    </div>

    <div class="mt-6">
        <div class="flex justify-between">
            <div class="">
                <input wire:model.debounce.500ms="q" type="search" placeholder="Search" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" />
            </div>
            <div class="mr-2">
                <input type="checkbox" class="mr-2 leading-tight" wire:model="active" />Active Only?
            </div>
        </div>
        <table class="table-auto w-full">
            <thead>
                <tr>
                    <th class="px-4 py-2">
                        <div class="flex items-center">
                            <button wire:click="sortBy('id')">ID</button>
                            <x-sort-icon sortField="id" :sort-by="$sortBy" :sort-asc="$sortAsc" />
                        </div>
                    </th>
                    <th class="px-4 py-2">
                        <div class="flex items-center">
                            <button wire:click="sortBy('name')">Name</button>
                            <x-sort-icon sortField="name" :sort-by="$sortBy" :sort-asc="$sortAsc" />
                        </div>
                    </th>
                    <th class="px-4 py-2">
                        <div class="flex items-center">
                            <button wire:click="sortBy('price')">Price</button>
                            <x-sort-icon sortField="price" :sort-by="$sortBy" :sort-asc="$sortAsc" />
                        </div>
                    </th>
                    @if(!$active)
                        <th class="px-4 py-2">
                            Status
                        </th>
                    @endif
                    <th class="px-4 py-2">
                        Actions
                    </th>
                </tr>
            </thead>
            <tbody>
                @foreach($items as $item)
                    <tr>
                        <td class="border px-4 py-2">{{ $item->id}}</td>
                        <td class="border px-4 py-2">{{ $item->name}}</td>
                        <td class="border px-4 py-2">{{ number_format($item->price, 2)}}</td>
                        @if(!$active)
                            <td class="border px-4 py-2">{{ $item->status ? 'Active' : 'Not-Active'}}</td>
                        @endif
                        <td class="border px-4 py-2">
                        <x-jet-button wire:click="confirmItemEdit( {{ $item->id}})" class="bg-orange-500 hover:bg-orange-700">
                            Edit
                        </x-jet-button>
                            <x-jet-danger-button wire:click="confirmItemDeletion( {{ $item->id}})" wire:loading.attr="disabled">
                                Delete
                            </x-jet-danger-button>
                        </td>
                    </tr>
                @endforeach
            </tbody>
        </table>
    </div>

    <div class="mt-4">
        {{ $items->links() }}
    </div>

    <x-jet-confirmation-modal wire:model="confirmingItemDeletion">
        <x-slot name="title">
            {{ __('Delete Item') }}
        </x-slot>

        <x-slot name="content">
            {{ __('Are you sure you want to delete Item? ') }}
        </x-slot>

        <x-slot name="footer">
            <x-jet-secondary-button wire:click="$set('confirmingItemDeletion', false)" wire:loading.attr="disabled">
                {{ __('Nevermind') }}
            </x-jet-secondary-button>

            <x-jet-danger-button class="ml-2" wire:click="deleteItem({{ $confirmingItemDeletion }})" wire:loading.attr="disabled">
                {{ __('Delete') }}
            </x-jet-danger-button>
        </x-slot>
    </x-jet-confirmation-modal>

    <x-jet-dialog-modal wire:model="confirmingItemAdd">
        <x-slot name="title">
            {{ isset( $this->item->id) ? 'Edit Item' : 'Add Item'}}
        </x-slot>

        <x-slot name="content">
            <div class="col-span-6 sm:col-span-4">
                <x-jet-label for="name" value="{{ __('Name') }}" />
                <x-jet-input id="name" type="text" class="mt-1 block w-full" wire:model.defer="item.name" />
                <x-jet-input-error for="item.name" class="mt-2" />
            </div>

            <div class="col-span-6 sm:col-span-4 mt-4">
                <x-jet-label for="price" value="{{ __('Price') }}" />
                <x-jet-input id="price" type="text" class="mt-1 block w-full" wire:model.defer="item.price" />
                <x-jet-input-error for="item.price" class="mt-2" />
            </div>

            <div class="col-span-6 sm:col-span-4 mt-4">
                <label class="flex items-center">
                    <input type="checkbox" wire:model.defer="item.status" class="form-checkbox" />
                    <span class="ml-2 text-sm text-gray-600">Active</span>
                </label>
            </div>
        </x-slot>

        <x-slot name="footer">
            <x-jet-secondary-button wire:click="$set('confirmingItemAdd', false)" wire:loading.attr="disabled">
                {{ __('Nevermind') }}
            </x-jet-secondary-button>

            <x-jet-danger-button class="ml-2" wire:click="saveItem()" wire:loading.attr="disabled">
                {{ __('Save') }}
            </x-jet-danger-button>
        </x-slot>
    </x-jet-dialog-modal>
</div>

And with that our functionality is complete and now when you go to http://127.0.0.1:8000/items, our final code will look like below:

You can see that we have successfully implemented all the functionality using Livewire, TailwindCSS using Jestream Components itself. We have also used a bit of Alpin JS, which is installed automatically by Jetsream, so that we use all the TALL Stack.

Laravel jetstream livewire github demo

You can check all the Code available on Github with full commits at https://github.com/saurabh85mahajan/tall_jetstream

Laravel livewire video tutorial

If you want to dig deeper, you can also check the Youtube Series which covers all the steps in detail starting from the installation process. Below is the First Video of the Series.

2 thoughts on “Laravel livewire crud tutorial using Tailwind modal

Leave a Reply

Your email address will not be published. Required fields are marked *