CRUD App with Form Objects & Modal Elements in Livewire 3

Build A CRUD Application Using Form Objects and Modal Wire Elements

Let’s build a CRUD App with Form Objects & Modal Elements in Livewire 3  for a simple “Book Library” system. This will allow users to add, view, edit, and delete books from their personal library.

Step 1: Setting Up the Project

  1. Install Laravel:
composer create-project laravel/laravel book-library

Install Livewire:

composer require livewire/livewire

Publish Livewire Assets:

php artisan livewire:publish

Step 2: Database Setup

  1. Configure your .env file with your database credentials.
  2. Create a migration for the books table:
php artisan make:migration create_books_table

Update the migration to have the following columns: title, author, genre, and published_date.

Schema::create('books', function (Blueprint $table) {

            $table->id();

            $table->string('title');

            $table->string('author');

            $table->string('genre');

            $table->date('published_date');

            $table->timestamps();

        });

Run the migration:

php artisan migrate

Book.php Model

Run the following command to create the model

php artisan make:model Book.php
 class Book extends Model
{
    use HasFactory;

    protected $fillable = ['title', 'author', 'genre', 'published_date'];

}

 

Step 3: Create Livewire Components

  1. Book Component:
php artisan make:livewire BookComponent

This command will create two files a blade file `book-component.blade.php` and a class `BookComponent.php`. Update them respectively…

Livewire Component (BookComponent.php):

<?php

namespace App\Livewire;

use App\Models\Book;

use Livewire\Component;

use App\Livewire\Forms\BookForm;

class BookComponent extends Component

{

    // Properties

    public BookForm $bookForm; // Form object for handling book data

    public $isModalOpen = false; // Controls the visibility of the modal

    public $books; // Stores the list of books

    public $editingBookId = null; // ID of the book being edited
    // Mount method - initializes the form when the component is loaded

    public function mount()

    {

        $this->bookForm = new BookForm($this, 'bookForm');

    }

    // Resets the form to its initial state

    public function resetForm()

    {

        $this->bookForm = new BookForm($this, 'bookForm');

    }

    // Opens the modal for adding or editing a book

    public function showModal()
    {
        $this->isModalOpen = true;
    }
   
    // Closes the modal and resets the editing state

    public function closeModal()

    {
        $this->isModalOpen = false;

        $this->editingBookId = null;
    }

   
    // Saves a new book to the database
    public function saveBook()
    {   // Validates the form data based on the rules defined in the BookForm
        $this->validate();
        // Check if there are any validation errors
        if (!$this->getErrorBag()->isEmpty()) {
            // Keep the modal open if there are errors
            $this->isModalOpen = true;
            return;
        }
        // Save the book data to the database
        $saved = $this->bookForm->save();
        // Check if the book was successfully saved

        if ($saved) {
            // Flash a success message to the session
            session()->flash('message', 'Book added successfully.');
            // Reset the form fields to their initial state
            $this->bookForm->resetForm();
            // Close the modal after successful save
            $this->isModalOpen = false;       
        }

    }
    // Loads a book's data into the form for editing

    public function editBook($id)
    {
        // Find the book in the database by its ID
        $book = Book::find($id);
        // Fill the book form with the data of the found book
        $this->bookForm->fill($book->toArray());
        // Set the editingBookId to the ID of the book being edited
        $this->editingBookId = $id;
        // Open the modal for editing the book
        $this->isModalOpen = true;
    }
   

    // Updates an existing book's data in the database
    public function updateBook()
    {
        // Validates the form data based on the rules defined in the BookForm
        $this->bookForm->validate();
      
        // Check if there are any validation errors
        if (!$this->getErrorBag()->isEmpty()) {
            // Keep the modal open if there are errors
            $this->isModalOpen = true;
            return;
        }
   
        // Find the book in the database using the editingBookId
        $book = Book::find($this->editingBookId);
        // Update the book record with the new form data
        $book->update($this->bookForm->toArray());
   
        // Reset the editingBookId to null after the update
        $this->editingBookId = null;
        // Flash a success message to the session
        session()->flash('message', 'Book updated successfully.');
        // Reset the form fields to their initial state
        $this->bookForm->resetForm();
        // Close the modal after successful update
        $this->isModalOpen = false;
    }
    // Deletes a book from the database

    public function deleteBook($id)
    {
        Book::find($id)->delete();
        session()->flash('message', 'Book deleted successfully.');
    }
    // Renders the component with the latest list of books
    public function render()
    {
        $this->books = Book::all();
        return view('livewire.book-component');
    }
}

Explanation:

The BookComponent class in this code is a Laravel Livewire component designed for managing books in a web application.

It includes functionalities for adding, editing, and deleting books, as well as displaying them in a list.

The component uses a modal for inputting book details, which can be opened and closed dynamically.

The BookForm object encapsulates the form handling logic, including validation and data submission.

The component also handles user feedback by flashing messages upon successful operations.

This makes it a comprehensive solution for book management in a Laravel-based application.

Step 4: Form Objects

In Livewire 3, one of the new features introduced is the concept of “Form Objects.” Form Objects are a way to encapsulate form data and validation rules in a single object, making it easier to manage and validate form data in Livewire components.

The role of a Form Object is to:

  1. Encapsulate Form Data: Instead of having multiple properties for each form field in your Livewire component, you can have a single Form Object that holds all the form data.
  2. Handle Validation: You can define validation rules directly in the Form Object, keeping your validation logic separate from your component logic.

How to Create and Use a Form Object:

Run the following command to create a form object.

php artisan livewire:form BookForm

The above command will create a file called app/Livewire/Forms/BookForm.php

Update it with the following code

<?php
namespace App\Livewire\Forms;

use Livewire\Form;
use App\Models\Book;
class BookForm extends Form
{
    // Form properties for each field in the book form
    public $title;
    public $author;
    public $genre;
    public $published_date;
    // Defines the validation rules for the form fields

    protected function rules(): array
    {
        return [
            'title' => 'required', // Title is required

            'author' => 'required', // Author is required

            'genre' => 'required', // Genre is required

            'published_date' => 'required|date', // Published date is required and must be a valid date
        ];
    }
    // Saves the book data to the database

    public function save()
    {
        $this->validate(); // Validates the form data against the defined rules
        // Creates a new book record in the database with the form data
        Book::create([
            'title' => $this->title,
            'author' => $this->author,
            'genre' => $this->genre,
            'published_date' => $this->published_date,
        ]);
        return true; // Indicates successful save operation
    }
    // Resets the form fields to their default state
    public function resetForm()
    {
        $this->title = '';
        $this->author = '';
        $this->genre = '';
        $this->published_date = null;
    }
}

 

Explanation:

  • Properties: The class has properties (title, author, genre, published_date) that correspond to the fields of the book form. These properties store the current state of the form fields.
  • Validation Rules: The rules method defines the validation criteria for each form field. For instance, all fields are marked as ‘required’, and the published_date must be a valid date.
  • Save Method: The save method first validates the form data. If validation passes, it creates a new Book record in the database using the Laravel Eloquent model. The method returns true to indicate a successful save operation.
  • Reset Form Method: The resetForm method is used to clear the form fields, resetting them to their initial state. This is useful after saving data or when you need to clear the form for a new entry.

This class encapsulates the logic for handling book data in the form, including validation, data saving, and resetting form fields, making it reusable and maintainable.

Step 5: Livewire Component Blade (book-component.blade.php):

Let’s now add code to our book-component blade.

<!-- Polls the component every 5 seconds to refresh data and manages the modal open/close state -->
<div wire:poll.5s x-data="{ isOpen: @entangle('isModalOpen') }">
    <!-- Button to open the modal for adding a new book -->
    <button @click="isOpen = true; @this.set('isModalOpen', true)" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded mb-3 ml-5 mt-5 transition duration-300">Add New Book</button>
  <!-- Modal that appears when 'isOpen' is true, with a fade transition effect -->
    <div x-cloak x-show="isOpen" x-transition:opacity.duration.500ms class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center" tabindex="-1">
        <div class="bg-white rounded-lg w-1/2">
            <div class="">
                <div class="bg-gray-200 p-3 flex justify-between items-center rounded-t-lg">
                   <!-- Displays 'Edit Book' if editing an existing book, otherwise 'Add New Book' -->
                    <h5 class="text-lg font-semibold">{{ $editingBookId ? 'Edit Book' : 'Add New Book' }}</h5>
                    <!-- Button to close the modal, changes color on hover for better user interaction -->
                    <button type="button" class="text-gray-700 hover:text-gray-900 transition duration-300" @click="isOpen = false; @this.set('isModalOpen', false)" aria-label="Close">
                        <i class="fas fa-times"></i>
                    </button>
                </div>
                <div class="p-5">
                  <!-- Submits the form to 'updateBook' if editing, otherwise to 'saveBook' -->
                    <form wire:submit="{{ $editingBookId ? 'updateBook' : 'saveBook' }}">
                        <!-- Title Field -->
                        <div class="mb-4">
                            <label class="block text-gray-700 text-sm font-bold mb-2">Title</label>
                            <input type="text" class="w-full p-2 border rounded" wire:model="bookForm.title">
                            @error('bookForm.title') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
                        </div>
                        <!-- Author Field -->
                        <div class="mb-4">
                            <label class="block text-gray-700 text-sm font-bold mb-2">Author</label>
                            <input type="text" class="w-full p-2 border rounded" wire:model="bookForm.author">
                            @error('bookForm.author') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
                        </div>
                        <!-- Genre Field -->
                        <div class="mb-4">
                            <label class="block text-gray-700 text-sm font-bold mb-2">Genre</label>
                            <input type="text" class="w-full p-2 border rounded" wire:model="bookForm.genre">
                            @error('bookForm.genre') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
                        </div>
                        <!-- Published Date Field -->
                        <div class="mb-4">
                            <label class="block text-gray-700 text-sm font-bold mb-2">Published Date</label>
                            <input type="date" class="w-full p-2 border rounded" wire:model="bookForm.published_date">
                            @error('bookForm.published_date') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
                        </div>
                        <!-- Submit Button -->
                        <button type="submit" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded transition duration-300">
                            {{ $editingBookId ? 'Update' : 'Add' }} Book
                        </button>
                    </form>
                </div>
            </div>
        </div>
    </div>
    <!-- Display Message After CRUD Operations -->
    @if (session()->has('message'))
    <div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mt-3" role="alert">
        {{ session('message') }}
    </div>
    @endif
    <!-- Books Table -->
    <table class="min-w-full bg-white mt-4 shadow-md rounded-lg">
        <thead>
            <tr>
                <th class="py-2 px-4 border-b text-left">Title</th>
                <th class="py-2 px-4 border-b text-left">Author</th>
                <th class="py-2 px-4 border-b text-left">Genre</th>
                <th class="py-2 px-4 border-b text-left">Published Date</th>
                <th class="py-2 px-4 border-b text-left">Actions</th>
            </tr>
        </thead>
        <tbody>
            @foreach($books as $book)
            <tr>
                <td class="py-2 px-4 border-b">{{ $book->title }}</td>
                <td class="py-2 px-4 border-b">{{ $book->author }}</td>
                <td class="py-2 px-4 border-b">{{ $book->genre }}</td>
                <td class="py-2 px-4 border-b">{{ $book->published_date }}</td>
                <td class="py-2 px-4 border-b flex space-x-2">
                    <button wire:click="editBook({{ $book->id }})" class="bg-yellow-500 text-white px-2 py-1 rounded">Edit</button>
                    <button wire:click="deleteBook({{ $book->id }})" class="bg-red-500 text-white px-2 py-1 rounded">Delete</button>
                </td>
            </tr>
            @endforeach
        </tbody>
    </table>
</div>
  1. Wire Directives (wire:poll.5s, wire:model, wire:click, wire:submit):
    • wire:poll.5s: This directive was used to automatically refresh the component every 5 seconds. It’s a Livewire feature that helps keep the data on the page up-to-date.
    • wire:model: This binds a form input to a component property. For example, wire:model=”bookForm.title” binds the input field to the bookForm.title property in the Livewire component.
    • wire:click: This is used to handle click events in Livewire. For example, wire:click=”editBook({{ $book->id }})” calls the editBook method in the Livewire component when the button is clicked, passing the book’s ID as a parameter.
    • wire:submit: This directive is used to handle form submissions. For example, wire:submit=”saveBook” triggers the saveBook method in the Livewire component when the form is submitted.
  2. Dynamic Data Binding ({{ $editingBookId ? ‘Edit Book’ : ‘Add New Book’ }}):
    • This is a Blade syntax used for rendering data dynamically. It checks if $editingBookId is set and changes the text displayed based on the condition. This is used to switch between ‘Edit Book’ and ‘Add New Book’ modes in the modal.
  3. Error Handling (@error directive):
    • The @error directive is used to display validation error messages. For example, @error(‘bookForm.title’) displays an error message if there’s a validation error for the title field in the bookForm.
  4. Session Flash Messages (session()->has(‘message’)):
    • This part checks if there is a flash message in the session (usually set after a CRUD operation like adding or updating a book) and displays it. It’s a way to give feedback to the user after an action is performed.
  5. Looping Through Data (@foreach($books as $book)):
    • This is a Blade directive used to loop through a collection of books. Each book’s details are displayed in a table row. This is how the list of books is rendered on the page.
  6. Alpine.js Directives (x-data, x-show, x-transition):
    • These directives are part of Alpine.js, a frontend JavaScript framework. They are used for handling the modal’s open/close state and transitions. While they are more frontend-related, they were initially intertwined with Livewire’s state management.

Step 6: Add TailwindCSS

To add tailwindCSS externally we can add a cdn link as follows to the Livewire3 app.blade.php…

<title>{{ $title ?? 'Livewire CRUD' }}</title>
        <script src="https://cdn.tailwindcss.com"></script>
    </head>
<body>
    {{ $slot }}
</body>
</html>

 

 

Sign up for free tutorials in your inbox.

We don’t spam! Read our privacy policy for more info.

Leave a Comment

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