Pavel Zaněk PavelZanek.com
Select language
Account

Example: How to create CRUD operations in Laravel 10 with Livewire (including tests)

A tutorial that includes the process of creating CRUD operations in Laravel using Livewire. The tutorial covers all the key steps, from creating a Livewire component, to creating a template for the component, to creating tests.

Published at 2023-06-29 by Pavel Zaněk

Estimated Reading Time: 16 minutes

Example: How to create CRUD operations in Laravel 10 with Livewire (including tests)

Table of Contents

We will step into the world of Laravel and Livewire, where we will learn how to create a Livewire component for CRUD operations. Laravel is one of the most popular PHP frameworks, and Livewire is its powerful supplement that allows you to create modern, dynamic interfaces directly in Laravel. CRUD operations - Create, Read, Update, and Delete - are the basic building blocks of any application. In this article, we will focus on how to create a Livewire component in Laravel that allows these basic operations to be performed. In addition, we will learn how to test these components to make our application robust and reliable. Prepare for a journey full of code, innovation, and best practices in web application development.

What is Livewire and Laravel

Before we get into the tutorial itself, where we will show you step by step how to create logic in the Livewire component, it is good to familiarize yourself with both frameworks. Perhaps there are newcomers among you who are hearing about Laravel or Livewire for the first time.

Laravel

Laravel is an open-source PHP framework, highly appreciated for its elegant syntax and the ability to facilitate web application development by providing tools for common tasks such as routing, authentication, sessions, and caching. Laravel is built on Symfony components and its source code is hosted on GitHub.

Livewire

Livewire, on the other hand, is a full-fledged framework for Laravel that allows creating dynamic interfaces simply and without the need to write JavaScript. Livewire utilizes the concept of components, similar to React or Vue, but written in pure PHP. This means that you can create complex, reactive applications with a single language.

Together, Laravel and Livewire form a powerful combination for web application development. Laravel provides the basic infrastructure and Livewire adds a dynamic layer, enabling developers to create interactive user interfaces with minimal code and complexity.

Introduction to our demonstration

To better understand the demonstration, first go through the previously created article on working with CRUD operations in Laravel itself, where, in addition to basic information (e.g., what is CRUD), you will learn parts of the code that are prerequisites for further steps in this tutorial (migration, model, actions, etc.). This tutorial then assumes that you already have the Laravel framework installed along with Laravel Livewire.

For testing the Livewire component, we will also use the Pest package, which we also utilized in the previous tutorial.

Creating a Livewire Component

Creating a Livewire component in Laravel is a process that is simple and straightforward. We start by opening the terminal and navigating to the root directory of our Laravel project. Here, we can create a new Livewire component using the following command:

php artisan make:livewire ComponentName

Replace "ComponentName" with the name of your component. This command creates two new files: a component class and an associated Blade view/template. The component class is located in "app/Http/Livewire" and the Blade view is in "resources/views/livewire".

The component class is where you define all the logic of the component. It can contain public properties that are automatically synchronized between the backend and frontend, and methods that can respond to user events.

The Blade view is where you define the HTML interface of the component. You can use any valid Blade syntax/directive here, and you can also access the public properties and methods of the component class.

Creating a Livewire component is therefore a matter of creating these two files and defining the necessary logic and interface. Once you have these files prepared, you can start using your Livewire component in any Laravel view using the Livewire directive:

@livewire('component-name')

Again, replace "component-name" with the name of your component. This directive inserts your Livewire component into your template.

Demonstration of Laravel Livewire Component for CRUD Operations

However, our goal is to create a component that will process data from the "example_items" table, which we created according to the previous tutorial. So let's take a look at what the code might look like in the component class and in the Blade template.

Livewire Component Class ("app/Http/Livewire/Examples")

<?php

namespace App\Http\Livewire\Examples;

use App\Actions\Examples\ExampleItems\CreateExampleItemAction;
use App\Actions\Examples\ExampleItems\RemoveExampleItemAction;
use App\Actions\Examples\ExampleItems\UpdateExampleItemAction;
use App\Enums\Examples\ExampleItemType;
use App\Models\Examples\ExampleItem;
use Illuminate\Validation\Rules\Enum;
use Livewire\Component;
use Livewire\WithPagination;

class ExampleItemManager extends Component
{
    use WithPagination;

    public $q;
    public $sortBy = 'created_at';
    public $sortAsc = false;

    public $item;
    public $exampleItemTypes;

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

    protected $queryString = [
        'q' => ['except' => ''],
        'sortBy' => ['except' => 'created_at'],
        'sortAsc' => ['except' => false],
    ];

    public function mount()
    {
        $this->exampleItemTypes = ExampleItemType::all();
    }

    public function render()
    {
        $items = ExampleItem::when($this->q, function($query) {
                return $query->where(function( $query) {
                    $query->where('title', 'like', '%' . $this->q . '%');
                });
            })
            ->orderBy($this->sortBy, $this->sortAsc ? 'ASC' : 'DESC')
            ->paginate(20);

        return view('livewire.examples.example-item-manager', [
            'items' => $items,
        ]);
    }

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

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

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

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

    public function saveItem() 
    {
        $validatedData = $this->validate([
            'item.title' => 'required|string|max:255',
            'item.body' => 'nullable|string',
            'item.is_active' => 'nullable|boolean',
            'item.type' => [
                'required',
                'string',
                'max:8',
                new Enum(ExampleItemType::class),
            ],
        ]);

        if(isset($this->item['id'])) {
            (new UpdateExampleItemAction())->execute(
                ExampleItem::find($this->item['id']),
                $validatedData['item']
            );

            $this->dispatchBrowserEvent('alert',[
                'type'=>'success',
                'message'=> __('Example Item was successfully updated.')
            ]);
        } else {
            (new CreateExampleItemAction())->execute($validatedData['item']);

            $this->dispatchBrowserEvent('alert',[
                'type'=>'success',
                'message'=> __('Example Item was successfully created.')
            ]);
        }

        $this->reset(['item']);
        $this->confirmingItemAdd = false;
    }

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

    public function deleteItem(ExampleItem $exampleItem) 
    {
        (new RemoveExampleItemAction())->execute($exampleItem);

        $this->confirmingItemDeletion = false;

        $this->dispatchBrowserEvent('alert',[
            'type'=>'success',
            'message'=> __('Example Item was successfully removed.')
        ]);
    }
}

Blade view for Livewire Component ("resources/views/views/livewire/examples")

<div class="p-4 border-b border-gray-200">

    <div class="mt-2 text-2xl flex justify-between">
        <div class="text-gray-900 dark:text-gray-100">{{ __('Example Item Manager') }}</div> 
        <div class="mr-2">
            <x-button wire:click="confirmItemAdd" class="bg-blue-500 hover:bg-blue-700">
                {{ __('Add New Item') }}
            </x-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="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5  dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
            </div>
        </div>

        <div class="flex flex-col">
            <div class="overflow-x-auto sm:-mx-6 lg:-mx-8">
                <div class="inline-block min-w-full py-2 sm:px-6 lg:px-8">
                    <div class="overflow-hidden">
                        <table class="min-w-full text-left text-sm font-light">
                            <thead class="border-b font-medium dark:border-neutral-500 dark:text-gray-100">
                                <tr>
                                    <th scope="col" class="px-6 py-4">
                                        <div class="flex items-center">
                                            <button wire:click="sortBy('id')">{{ __('ID') }}</button>
                                            @if($sortBy=='id')
                                                @if($sortAsc)
                                                 <span class="ml-2"><i class="fas fa-sort-up"></i></span>
                                                @else
                                                 <span class="ml-2"><i class="fas fa-sort-down"></i></span>
                                                @endif
                                            @endif
                                        </div>
                                    </th>
                                    <th scope="col" class="px-6 py-4">
                                        <div class="flex items-center">
                                            <button wire:click="sortBy('title')">{{ __('Title') }}</button>
                                            @if($sortBy=='title')
                                                @if($sortAsc)
                                                 <span class="ml-2"><i class="fas fa-sort-up"></i></span>
                                                @else
                                                 <span class="ml-2"><i class="fas fa-sort-down"></i></span>
                                                @endif
                                            @endif
                                        </div>
                                    </th>
                                    <th scope="col" class="px-6 py-4">
                                        <div class="flex items-center">
                                            <button wire:click="sortBy('is_active')">{{ __('Is active') }}</button>
                                            @if($sortBy=='is_active')
                                                @if($sortAsc)
                                                 <span class="ml-2"><i class="fas fa-sort-up"></i></span>
                                                @else
                                                 <span class="ml-2"><i class="fas fa-sort-down"></i></span>
                                                @endif
                                            @endif
                                        </div>
                                    </th>
                                    <th scope="col" class="px-6 py-4">
                                        <div class="flex items-center">
                                            <button wire:click="sortBy('type')">{{ __('Type') }}</button>
                                            @if($sortBy=='type')
                                                @if($sortAsc)
                                                 <span class="ml-2"><i class="fas fa-sort-up"></i></span>
                                                @else
                                                 <span class="ml-2"><i class="fas fa-sort-down"></i></span>
                                                @endif
                                            @endif
                                        </div>
                                    </th>
                                    <th scope="col" class="px-6 py-4">
                                        <div class="flex items-center">
                                            <button wire:click="sortBy('created_at')">{{ __('Created at') }}</button>
                                            @if($sortBy=='created_at')
                                                @if($sortAsc)
                                                 <span class="ml-2"><i class="fas fa-sort-up"></i></span>
                                                @else
                                                 <span class="ml-2"><i class="fas fa-sort-down"></i></span>
                                                @endif
                                            @endif
                                        </div>
                                    </th>
                                    <th scope="col" class="px-6 py-4">
                                        <div class="flex items-center">
                                            <button wire:click="sortBy('updated_at')">{{ __('Updated at') }}</button>
                                            @if($sortBy=='updated_at')
                                                @if($sortAsc)
                                                 <span class="ml-2"><i class="fas fa-sort-up"></i></span>
                                                @else
                                                 <span class="ml-2"><i class="fas fa-sort-down"></i></span>
                                                @endif
                                            @endif
                                        </div>
                                    </th>
                                    <th scope="col" class="px-6 py-4">
                                        {{ __('Actions') }}
                                    </th>
                                </tr>
                            </thead>
                            <tbody>
                                @forelse($items as $item)
                                    <tr class="border-b transition duration-300 ease-in-out dark:text-gray-100 hover:bg-neutral-100 dark:border-neutral-500 dark:hover:bg-neutral-600 dark:hover:text-gray-200">
                                        <td class="whitespace-nowrap px-6 py-4 font-medium">{{ $item->id }}</td>
                                        <td class="whitespace-nowrap px-6 py-4">{{ $item->title }}</td>
                                        <td class="whitespace-nowrap px-6 py-4">{{ $item->is_active ? __('Yes') : __('No') }}</td>
                                        <td class="whitespace-nowrap px-6 py-4">{{ $item->type->value }}</td>
                                        <td class="whitespace-nowrap px-6 py-4">{{ $item->created_at->diffForHumans() }}</td>
                                        <td class="whitespace-nowrap px-6 py-4">{{ $item->updted_at?->diffForHumans() }}</td>
                                        <td class="whitespace-nowrap px-6 py-4">
                                            <a href="{{ route('example-items.show', ['language' => app()->getLocale(), 'example_item' => $item->id]) }}" class="inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150">
                                                {{ __('Show') }}
                                            </a>
                                            <x-button wire:click="confirmItemEdit( {{ $item->id }})" class="inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150">
                                                {{ __('Edit') }}
                                            </x-button>
                                            <x-danger-button wire:click="confirmItemDeletion({{ $item->id }})" wire:loading.attr="disabled">
                                                {{ __('Delete') }}
                                            </x-danger-button>
                                        </td>
                                    </tr>
                                @empty
                                    <tr class="border-b transition duration-300 ease-in-out dark:text-gray-100 hover:bg-neutral-100 dark:border-neutral-500 dark:hover:bg-neutral-600 dark:hover:text-gray-200">
                                        <td class="whitespace-nowrap px-6 py-4 font-medium">
                                            {{ __('No item found') }}
                                        </td>
                                    </tr>
                                @endforelse
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </div>

    </div>

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

    <x-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="">
                <x-label for="title" value="{{ __('Title') }}" />
                <x-input id="title" type="text" wire:model.defer="item.title" class="mt-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
                <x-input-error for="item.title" class="mt-2" />
            </div>

            <div class="mt-4">
                <x-label for="body" value="{{ __('Body') }}" />
                <textarea wire:model.defer="item.body"
                    id="body"
                    class="block mt-2 p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                    rows="4"></textarea>
                <x-input-error for="item.body" class="mt-2" />
            </div>

            <div class="mt-4">
                <div class="flex items-center justify-start">
                    <x-input wire:model.defer="item.is_active" id="is-active" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
                    <x-label for="is-active" value="{{ __('Is active') }}" class="ml-2" />
                </div>
                <x-input-error for="item.is_active" class="mt-2" />
            </div>

            <div class="mt-4">
                <x-label for="type" value="{{ __('Type') }}" />
                <select wire:model.defer="item.type" id="type" class="mt-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
                    <option selected>{{ __('Choose a type') }}</option>
                    @foreach ($exampleItemTypes as $exampleItemType)
                        <option value="{{ $exampleItemType['value'] }}">
                            {{ $exampleItemType['name'] }}
                        </option>
                    @endforeach
                </select>
                <x-input-error for="item.type" class="mt-2" />
            </div>
        </x-slot>

        <x-slot name="footer">
            <x-secondary-button wire:click="$set('confirmingItemAdd', false)" wire:loading.attr="disabled">
                {{ __('Nevermind') }}
            </x-secondary-button>
 
            <x-button class="ml-2 bg-blue-500 hover:bg-blue-700" wire:click="saveItem()" wire:loading.attr="disabled">
                {{ __('Save') }}
            </x-button>
        </x-slot>
    </x-dialog-modal>

    <x-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-secondary-button wire:click="$set('confirmingItemDeletion', false)" wire:loading.attr="disabled">
                {{ __('Nevermind') }}
            </x-secondary-button>
 
            <x-danger-button class="ml-2" wire:click="deleteItem({{ $confirmingItemDeletion }})" wire:loading.attr="disabled">
                {{ __('Delete') }}
            </x-danger-button>
        </x-slot>
    </x-confirmation-modal>
</div>

Testing Livewire Component

Testing is a crucial part of software development and Livewire is no exception. Laravel provides excellent testing tools that we can also use when testing our Livewire components.

For testing Livewire components, we can use Laravel's testing framework PHPUnit. Laravel provides a test() method, which we can use to create test cases for our components. However, as I am an advocate of simplicity and prefer more readable code, we will use the Pest package (with the use of a plugin for Livewire). After its installation, the component test might look like this:

<?php

use App\Enums\Examples\ExampleItemType;
use App\Models\Examples\ExampleItem;
use Livewire\Livewire;

it('can render the livewire item manager component', function () {
    Livewire::test('examples.example-item-manager')
        ->assertStatus(200);
});

it('can add new item', function () {
    Livewire::test('examples.example-item-manager')
        ->set('item.title', 'Test item')
        ->set('item.body', 'Test body')
        ->set('item.is_active', true)
        ->set('item.type', ExampleItemType::TYPE2->value)
        ->call('saveItem')
        ->assertSee('Test item');
});

it('can edit existing item', function () {
    $item = ExampleItem::factory()->create();

    Livewire::test('examples.example-item-manager')
        ->call('confirmItemEdit', $item->id)
        ->set('item.title', 'Test edit')
        ->call('saveItem')
        ->assertSee('Test edit');
});

it('can delete an existing item', function () {
    $item = ExampleItem::factory()->create();

    Livewire::test('examples.example-item-manager')
        ->call('confirmItemDeletion', $item->id)
        ->call('deleteItem', $item->id)
        ->assertDontSee($item->title);
});

it('can search for items', function () {
    $item1 = ExampleItem::factory()->create(['title' => 'First Item']);
    $item2 = ExampleItem::factory()->create(['title' => 'Second Item']);

    Livewire::test('examples.example-item-manager')
        ->set('q', 'First')
        ->assertSee($item1->title)
        ->assertDontSee($item2->title);
});

it('can sort items', function () {
    $item1 = ExampleItem::factory()->create(['title' => 'First Item']);
    $item2 = ExampleItem::factory()->create(['title' => 'Second Item']);

    $component = Livewire::test('examples.example-item-manager');

    // sorting is ascending
    $component->set('sortBy', 'title')->set('sortAsc', true)->call('render');
    $this->assertEquals([$item1->id, $item2->id], $component->viewData('items')->pluck('id')->toArray());

    // now sort descending
    $component->set('sortBy', 'title')->set('sortAsc', false)->call('render');
    $this->assertEquals([$item2->id, $item1->id], $component->viewData('items')->pluck('id')->toArray());
});

Testing is essential to ensure the quality of our code and to verify that our application works as it should. Laravel and Livewire provide all the tools we need to create robust and reliable tests for our applications.

Laravel Livewire CRUD Test

In Conclusion

In this article, we have looked in detail at how to create a Livewire component in Laravel for CRUD operations including testing. We have gone through the basics of Laravel and Livewire, we have created a Livewire component, we have implemented CRUD operations, and we have learned how to test our component.

Important points that emerge from this tutorial are:

  • Laravel and Livewire are a powerful combination for web application development.
  • CRUD operations are basic operations that we can perform on our data.
  • Testing is essential to ensure the quality of our code and to verify that our application works as it should.

I hope this tutorial will help you create your own Livewire components and that it will inspire you to further explore the possibilities that Laravel and Livewire offer.

Share:
5 / 5
Total votes: 1
You have not rated yet.
Pavel Zaněk

Full-stack developer & SEO consultant

Pavel Zaněk is an experienced full-stack developer with expertise in SEO and programming in Laravel. His skills include website optimization, implementing effective strategies to increase traffic and improve search engine rankings. Pavel is an expert in Laravel and its related technologies, including Livewire, Vue.js, MariaDB, Redis, TailwindCSS/Bootstrap and much more. In addition to his programming skills, he also has a strong background in VPS management, enabling him to handle complex server-side challenges. Pavel is a highly motivated and dedicated professional who is committed to delivering exceptional results. His goal is to help clients achieve success in the online space and achieve their goals with the help of the best web technologies and SEO strategies.

Comments

No comment has been added yet.

Add a comment

Suggested Articles

How to create RSS feed in Laravel framework

Published at 2023-08-10 by Pavel Zaněk

Estimated Reading Time: 10 minutes

laravel

Guide to creating RSS feed in Laravel framework without external packages. From basic principles to advanced techniques. Ideal for developers looking for an efficient and secure solution.

Continue reading

Your experience on this site will be improved by allowing cookies. - Cookies Policy