Livewire 3 CRUD with Form Objects and Modal Wire Elements

Livewire v3 introduced Form Objects to offload the field logic from the Component. In this tutorial, we'll build the create/edit modal forms powered by the Wire Elements package and reuse the same Livewire component and Form Object.

modal with unique rule


Preparation: Laravel Project

For the Laravel project visual design, we will use our own starter kit Laravel Breeze Pages Skeleton.

In the Model Product we will have two DB fields: name and description.

database/migrations/xxx_create_products_table.php:

public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description');
$table->timestamps();
});
}

app/Models/Product.php:

class Product extends Model
{
protected $fillable = [
'name',
'description',
];
}

Install Livewire and Customize Breeze

First, of course, we need to install install Livewire.

composer require livewire/livewire

Next, because Alpine.js is baked into the core of Livewire, we need to remove it from our Breeze-powered skeleton. If you don't use Laravel Breeze, you can skip this step.

resources/js/app.js:

import './bootstrap';
 
import Alpine from 'alpinejs';
 
window.Alpine = Alpine;
 
Alpine.start();

And recompile assets.

npm run prod

The last thing is changing the main layout path.

Livewire looks for a layout in resources/views/components/layouts/app.blade.php, but our Breeze-based Starter kit has it in a different place, so we need to set it in the Livewire config.

php artisan livewire:publish --config

config/livewire.php:

return [
// ...
'layout' => 'components.layouts.app',
'layout' => 'layouts.app',
// ...
];

And that's it, we can use Livewire in our project. Now, let's create a Livewire component to show the products list.


Livewire Component: Products List

php artisan make:livewire ProductList

app/Livewire/ProductList.php:

use App\Models\Product;
use Illuminate\Contracts\View\View;
 
class ProductList extends Component
{
public function render(): View
{
return view('livewire.product-list', [
'products' => Product::all(),
]);
}
}

Next, let's add a Route leading to that Livewire component. We will use it as a full-page Livewire component, without Laravel Controller:

routes/web.php

use App\Livewire\ProductList;
 
// ...
 
Route::get('products', ProductList::class)->name('products');

Let's add a link to the navigation and show the products table.

resources/views/layouts/navigation.blade.php:

// ...
 
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<x-nav-link :href="route('users.index')" :active="request()->routeIs('users.index')">
{{ __('Users') }}
</x-nav-link>
<x-nav-link :href="route('products')" :active="request()->routeIs('products')">
{{ __('Products') }}
</x-nav-link>
</div>
 
// ...

resources/views/livewire/product-list.blade.php:

<div>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800">
{{ __('Products') }}
</h2>
</x-slot>
 
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
<div class="p-6 overflow-hidden overflow-x-auto bg-white border-b border-gray-200">
 
<div class="min-w-full align-middle">
<table class="min-w-full border divide-y divide-gray-200">
<thead>
<tr>
<th class="px-6 py-3 text-left bg-gray-50">
<span class="text-xs font-medium leading-4 tracking-wider text-gray-500 uppercase">Name</span>
</th>
<th class="px-6 py-3 text-left bg-gray-50">
<span class="text-xs font-medium leading-4 tracking-wider text-gray-500 uppercase">Description</span>
</th>
<th class="px-6 py-3 text-left bg-gray-50">
</th>
</tr>
</thead>
 
<tbody class="bg-white divide-y divide-gray-200 divide-solid">
@forelse($products as $product)
<tr class="bg-white">
<td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap">
{{ $product->name }}
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap">
{{ $product->description }}
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap">
{{-- Edit Button --}}
</td>
</tr>
@empty
<tr class="bg-white">
<td colspan="3" class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap">
No products found.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
 
</div>
</div>
</div>
</div>
</div>

products table


Modal with Form Object

Now, let's create a component that will be a modal and use Form Object for properties, validation, etc.

php artisan make:livewire ProductModal
php artisan livewire:form ProductForm

We will use a package wire-elements/modal for modal.

composer require wire-elements/modal:^2.0

To make a Livewire component as a modal, it needs to extend LivewireUI\Modal\ModalComponent instead of Livewire\Component.

app/Livewire/ProductModal.php:

use Livewire\Component;
use Illuminate\Contracts\View\View;
use LivewireUI\Modal\ModalComponent;
 
class ProductModal extends Component
class ProductModal extends ModalComponent
{
public function render(): View
{
return view('livewire.product-form');
}
}

And let's add a form to the Blade file.

resources/views/livewire/product-form.blade.php:

<div class="p-6">
<form wire:submit="save">
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" class="mt-1 block w-full" type="text" />
</div>
 
<div class="mt-4">
<x-input-label for="description" :value="__('Description')" />
<textarea id="description" class="mt-1 w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:focus:border-indigo-600"></textarea>
</div>
 
<div class="mt-4">
<x-primary-button>
Save
</x-primary-button>
</div>
</form>
</div>

Notice: we're reusing Blade components like x-text-input and x-primary-button from the default Laravel Breeze.

Next, we need buttons to open a modal.

resources/views/livewire/product-list.blade.php:

// ...
 
<x-primary-button wire:click="$dispatch('openModal', { component: 'product-modal' })" class="mb-4">
New Product
</x-primary-button>
 
// ...
@forelse($products as $product)
<tr class="bg-white">
<td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap">
{{ $product->name }}
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap">
{{ $product->description }}
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap">
<x-secondary-button wire:click="$dispatch('openModal', { component: 'product-modal', arguments: { product: {{ $product }} }})">
Edit
</x-secondary-button>
</td>
</tr>
@empty
// ...

After pressing the New Product button, we see an opened modal with the form inside.

empty product form


Save a New Product

Now that we can open a modal, let's create and update a product.

First, we must add properties in the Form Object and bind them to inputs.

app/Livewire/Forms/ProductForm.php:

class ProductForm extends Form
{
public string $name = '';
public string $description = '';
}

app/Livewire/ProductModal.php:

class ProductModal extends ModalComponent
{
public Forms\ProductForm $form;
 
// ...
}

resources/views/livewire/product-modal.blade.php:

<div class="p-6">
<form wire:submit="save">
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" class="mt-1 block w-full" type="text" />
<x-text-input wire:model="form.name" id="name" class="mt-1 block w-full" type="text" />
<x-input-error :messages="$errors->get('form.name')" class="mt-2" />
</div>
 
<div class="mt-4">
<x-input-label for="description" :value="__('Description')" />
<textarea id="description" class="mt-1 w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:focus:border-indigo-600"></textarea>
<textarea wire:model="form.description" id="description" class="mt-1 w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:focus:border-indigo-600"></textarea>
<x-input-error :messages="$errors->get('form.description')" class="mt-2" />
</div>
 
<div class="mt-4">
<x-primary-button>
Save
</x-primary-button>
</div>
</form>
</div>

For saving, we defined the save method in the wire:submit directive. The whole creating and updating logic will be in the Form Object. In the modal component we just need to call it.

app/Livewire/ProductForm.php:

class ProductForm extends Form
{
public string $name = '';
public string $description = '';
 
public function save(): void
{
$this->validate();
 
Product::create($this->only(['name', 'description']));
 
$this->reset();
}
 
public function rules(): array
{
return [
'name' => [
'required',
],
'description' => [
'required'
],
];
}
}

We are validating inputs and creating a DB record.

app/Livewire/ProductModal.php:

class ProductModal extends ModalComponent
{
public Forms\ProductForm $form;
 
public function save(): void
{
$this->form->save();
 
$this->closeModal();
}
 
// ...
}

After saving a product, we close a modal. And, of course, we need to refresh the table automatically.

app/Livewire/ProductModal.php:

class ProductModal extends ModalComponent
{
public Forms\ProductForm $form;
 
public function save(): void
{
$this->form->save();
 
$this->closeModal();
 
$this->dispatch('refresh-list');
}
 
// ...
}

app/Livewire/ProductList.php:

use Livewire\Attributes\On;
 
class ProductList extends Component
{
// ...
 
#[On('refresh-list')]
public function refresh() {}
}

new product created


Product Edit: Form and Save Data

Now, let's add logic to edit the product. First, we need to set properties only if a product is passed.

app/Livewire/ProductModal.php:

use App\Models\Product;
 
class ProductModal extends ModalComponent
{
public ?Product $product = null;
public Forms\ProductForm $form;
 
public function mount(Product $product = null): void
{
if ($product->exists) {
$this->form->setProduct($product);
}
}
 
// ...
}

app/Livewire/Forms/ProductForm.php:

use App\Models\Product;
 
class ProductForm extends Form
{
public ?Product $product = null;
 
public string $name = '';
public string $description = '';
 
public function setProduct(?Product $product = null): void
{
$this->product = $product;
$this->name = $product->name;
$this->description = $product->description;
}
 
// ...
}

For updating a record in the DB, we need to do a simple check and, based on it, create or update a record.

app/Livewire/Forms/ProductForm.php:

class ProductForm extends Form
{
// ...
 
public function save(): void
{
$this->validate();
 
if (empty($this->product)) {
Product::create($this->only(['name', 'description']));
} else {
$this->product->update($this->only(['name', 'description']));
}
 
$this->reset();
}
 
// ...
}

edit product modal

Now, we can create and update products using the same Livewire modal component and the same Form Object.


Making Name Unique

Now, let's add a unique rule for the product name.

app/Livewire/Forms/ProductForm.php:

use Illuminate\Validation\Rule;
 
class ProductForm extends Form
{
// ...
 
public function rules(): array
{
return [
'name' => [
'required',
Rule::unique('products', 'name')->ignore($this->component->product),
],
'description' => [
'required'
],
];
}
}

The crucial part here is how to ignore record when editing a product. We can access properties from the component in the Livewire component using $this->component.


Extra Validation Tip: Prefix "form."

When validation rules are triggered now because we use Form Object, the attributes are called with a prefix of form.

bad validation names

We can fix it by just adding a validationAttributes method.

app/Livewire/Forms/ProductForm.php:

class ProductForm extends Form
{
// ...
 
public function validationAttributes(): array
{
return [
'name' => 'name',
'description' => 'description',
];
}
}

Now, they are shown as expected.

good validation names

The full code can be found in the GitHub repository.


You may also be interested in our PREMIUM course Livewire 3 From Scratch

avatar

The Git repo link is not working!

avatar

maybe mr pivolas forget to make it public. this repo are similar https://github.com/LaravelDaily/Livewire-3-Course-Demo

avatar

@adithyaricky your mentioned is public

avatar

Link is still not fixed.

https://github.com/LaravelDaily/Livewire-3-CRUD-Modal-Unique-Demo

avatar

I've identified errors in the tutorial, and my code isn't functioning correctly. I'd like to access the source code. Could you please correct the link as soon as possible?

avatar

Will be fixed today

avatar

Now it's public

avatar

i don't see the link for the video. Do you have video tutorial of this topic ?

avatar

Povilas have plenty videos on YouTube about livewire

avatar
You can use Markdown
avatar

Dear Povilas, I noticed an issue in your code within the app/Livewire/ProductModal.php file. It appears that there is an incorrect view being used inside the render() method. Similarly, there seems to be a filename discrepancy in resources/views/livewire/product-form.blade.php, possibly causing issues.

I cloned your code and tested the application, but it doesn't seem to open the modal as expected.

I've had a somewhat frustrating experience with the course "Livewire 3 From Scratch: Practical Course." As a result, I decided to jump into this CRUDy article to fill in the gaps in my understanding of Livewire. However, I found myself facing similar challenges with this article as well. I'm hoping there's a beginner-friendly CRUDy course available for Livewire v3 that can help me grasp the concepts more effectively. Thank you for your assistance!

avatar

Add

@livewire('wire-elements-modal')

inside the body section of resources/views/layouts/app.blade.php

avatar

resources/views/livewire/product-form.blade.php

should be kept as

resources/views/livewire/product-modal.blade.php

avatar
You can use Markdown
avatar

Can you show the navigation link when collapsed code also? The demo is fine on a computer but I have no links when on mobile?

avatar

You need to add them in the navigation.blade.php

avatar

The example shows the navigation but it didn't put it into the drop-link area.

avatar

Yes because we aren't building a full app. It's just to show a concept how to do something. If you need to links for mobile feel free to do that it's in the same

avatar
You can use Markdown
avatar

It appears that this step-by-step guide is missing some information. I'm not sure if it's intentional to encourage research and learning, or if it's simply an oversight. First, what are the routes? Are we using Controllers to call the Livewire pages/components, or are we calling them directly without the use of controllers? How are Data Models being passed to the view?

I am a subscriber to this website, but sometimes I struggle to make the examples work."

avatar

For this tutorial no contdollera are used this is a full page component. For data the route model binding is being used. Also the source code is available you can check

avatar

define route in web.php as Route::get('/products', ProductList::class)->name('products');

avatar
You can use Markdown
avatar

Can you please Redo this Tutorial using the 2.0.7 version of LivewireUI\Modal\ModalComponent I just can not get this Tutorial to work It is to outdated. not even your Github Repository will work for me.

avatar

This is a new tutorial only a month old and uses the 2.o.7 version of wire-elements/modal.

avatar
You can use Markdown
avatar

Hy! I would like to replace the listing section in the description with the Livewire PowerGrid listing. Is there a description available regarding this? Thank you.

avatar

No, we don't have any tutorials using powergrid. Maybe Povilas has a YouTube video. Just check their docs

avatar

Okay, thank you.

avatar
You can use Markdown
avatar

Will there be a solution for the delete function in the near future?

avatar

What do you need about it? Create a delete method and pass id to it. In that method delete. There are a couple livewire courses that do this.

avatar

Then in the title of the article in the CRUD acronym you need to remove the letter D :)

avatar
You can use Markdown
avatar

How to adjust the width or size of the modal

<div class="overflow-x-auto">
    <table class="min-w-full bg-white divide-y divide-gray-200">
        <thead>
        <tr>
            <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                Category
            </th>
            <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                Total
            </th>
            <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                Reserved
            </th>
            <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                Available
            </th>
        </tr>
        </thead>
        <tbody class="bg-white divide-y divide-gray-200">
        <!-- Example row (repeat for each item) -->
        <tr>
            <td class="px-6 py-4 whitespace-nowrap">
                <!-- Category value -->
            </td>
            <td class="px-6 py-4 whitespace-nowrap">
                <!-- Total value -->
            </td>
            <td class="px-6 py-4 whitespace-nowrap">
                <!-- Reserved value -->
            </td>
            <td class="px-6 py-4 whitespace-nowrap">
                <!-- Available value -->
            </td>
        </tr>
        <!-- Add more rows as needed... -->
        </tbody>
    </table>
</div>

i have design here that table is horizontally scrollable how to make it reponsive

avatar

For modal refer to the docs of the package. For table you will need to learn CSS.

avatar

no matter what i do to my modal it will not change its width, do i need the pro version?

i already did public static function modalMaxWidth(): string { return '2xl'; }

avatar

Did you compile your assets after changin the width?

Keep in mind: With any css changes you need to run npm run build to compile them into "available" assets, otherwise - no class will be added.

avatar

Just adding a method like that shouldn't change the width if tailwind doesn't compile that class. Check the docs this part https://github.com/wire-elements/modal#building-tailwind-css-for-production

avatar

fixed it, the problem is the minimum width not the maximum

avatar

having the same problem with modal width. how did you solve yours with minimum width ?

avatar

Refer to package documentation

avatar
You can use Markdown
avatar

Perhaps a general question about Laravel model methods, but how would one verify the $model->update() (or $model->save) / $model->delete() worked and then send that back to the Livewire component for confirmation?

avatar
You can use Markdown
avatar
You can use Markdown

Recent New Courses