Published on

CRUD with Laravel Livewire 3

For some individuals, constructing applications using full-stack can feel quite challenging, especially when dealing with multiple frameworks simultaneously like Vue.js or React.js. The good news is, there is now a full-stack framework available that allows us to create application interfaces using just Laravel – it's called Laravel Livewire.

What is Laravel Livewire

Laravel Livewire is a framework for Laravel that enables us to build interactive web interfaces using server-side components effortlessly. With Livewire, we can merge server-side and client-side code within a single component, thereby reducing complexity and enhancing productivity in web interface development.

Key Features of Laravel Livewire

Laravel Livewire offers several prominent features:

  1. Server-Side Based Components: We can create components using PHP code to manage logic and presentation of elements on web pages. These components can directly interact with data and events on the server side.
  2. Automatic Reactivity: Livewire automatically handles data changes and interactions. When there are interactions or data changes in components, Livewire will automatically update the web interface without the need for additional JavaScript code.
  3. Real-Time Communication: Livewire utilizes AJAX technology for real-time communication with the server without the need to reload the entire page. This enables us to build more responsive and interactive web applications.
  4. Integration with Laravel: Livewire is designed to seamlessly integrate with the Laravel framework. We can leverage Laravel features like Eloquent, Validation, Middleware, and more within Livewire components.
  5. SEO Friendly: By utilizing server-side rendering, Livewire generates web pages that are more SEO-friendly. The main content is available in the HTML source code when the page is initially loaded, contributing to better search engine optimization.

Laravel Livewire offers an easier way to build interactive web applications compared to using JavaScript frameworks like Vue or React. This allows Laravel developers to focus on business logic and development without spending excessive time dealing with the complexities of user interface management.

As of August 25, 2023, Laravel Livewire v3.0.0 has been released with various updates, which you can explore in detail in its official documentation. Since the draft version of this tutorial uses an older Livewire version, we will switch to using Laravel Livewire 3 in the released version of this tutorial.

Overview

In this Laravel 10 tutorial, we will explore the use of Laravel Livewire 3 to build a simple project with CRUD functionality. As for the data used in this project, we will employ the usual post data. The ultimate goal, as the title of this tutorial suggests, is for our project to perform CRUD operations, enabling it to display post data, create new posts, update existing posts, and delete post data.

Step 1 - Setting Up the Project

First, let's create a new Laravel project using composer. Open your terminal and execute the following command below.

composer create-project --prefer-dist laravel/laravel crud_livewire

Step 2 - Configure the Database Settings

Next, navigate to the root directory of our project using the command below.

cd crud_livewire

After that, open the .env file in a text editor and adjust the database credentials accordingly.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=db_crud_livewire
DB_USERNAME=admin
DB_PASSWORD=password

Make sure the credentials and the database name are in line with what we are going to use. Then, save the .env file again.

Step 3 - Installing Laravel Livewire 3

The next step is to install the Laravel Livewire 3 package. Open the terminal again, then install livewire through composer.

composer require livewire/livewire

Wait until the process of downloading the livewire package is complete.

Let's check in the composer.json file. At the time of writing this tutorial, the installed version of Livewire is version 3.

    "require": {
        "php": "^8.1",
        "guzzlehttp/guzzle": "^7.2",
        "laravel/framework": "^10.10",
        "laravel/sanctum": "^3.2",
        "laravel/tinker": "^2.8",
        "livewire/livewire": "^3.0"
    },

Step 4 - Create Model and Migration Files

Our project preparation is complete, the next step is to create model and migration files. We will generate the model and migration files using the following artisan command.

php artisan make:model Post -m

Output when we run the command above:

$ php artisan make:model Post -m

   INFO  Model [app/Models/Post.php] created successfully.

   INFO  Migration [database/migrations/2023_07_28_025654_create_posts_table.php] created successfully.

After the migration file is successfully generated, let's open the database/migrations/20xx_xx_xx_xxxxxx_create_posts_table.php file, and then adjust it with the following code snippet:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content');
            $table->string('slug');
            $table->smallInteger('status');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};


Once done, save the migration file again and don't forget to run the migration using the artisan command.

php artisan migrate

Output:

$ php artisan migrate

   INFO  Preparing database.

  Creating migration table ......................................... 11ms DONE

   INFO  Running migrations.

  2014_10_12_000000_create_users_table ............................. 13ms DONE
  2014_10_12_100000_create_password_reset_tokens_table ............. 27ms DONE
  2019_08_19_000000_create_failed_jobs_table ....................... 24ms DONE
  2019_12_14_000001_create_personal_access_tokens_table ............ 30ms DONE
  2023_07_28_025654_create_posts_table ............................. 13ms DONE

Alright, the posts table is now ready in our database.

Next, let's open the second file, which is the model file app/Models/Post.php. In this file, we'll add the $fillable attribute to manage the allowed mass assignments.

<?php

namespace App\Models;

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

class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'title', 'content', 'slug', 'status'
    ];
}


Step 5 - Create Post Component

In this step, we're diving into using Livewire. Now, let's create a Livewire component using the command below.

php artisan make:livewire post

Output:

$ php artisan make:livewire post
 COMPONENT CREATED  🤙

CLASS: app/Http/Livewire/Post.php
VIEW:  resources/views/livewire/post.blade.php

Next, open the file app/Http/Livewire/Post.php, then add the following lines of code.

<?php

namespace App\Livewire;

use Illuminate\Support\Str;
use Livewire\Component;

class Post extends Component
{
    /**
     * define public variable
     */
    public $title, $content, $postId, $slug, $status, $updatePost = false, $addPost = false;

    /**
     * List of add/edit form rules
     */
    protected $rules = [
        'title' => 'required',
        'content' => 'required',
        'status' => 'required'
    ];

    /**
     * Reseting all inputted fields
     * @return void
     */
    public function resetFields()
    {
        $this->title = '';
        $this->content = '';
        $this->status = 1;
    }

    /**
     * render the post data
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
     */
    public function render()
    {
        $posts = \App\Models\Post::latest()->get();
        return view('livewire.post', compact('posts'));
    }

    /**
     * Open Add Post form
     * @return void
     */
    public function create()
    {
        $this->resetFields();
        $this->addPost = true;
        $this->updatePost = false;
    }

    /**
     * store the user inputted post data in the posts table
     * @return void
     */
    public function store()
    {
        $this->validate();
        try {
            \App\Models\Post::create([
                'title' => $this->title,
                'content' => $this->content,
                'status' => $this->status,
                'slug' => Str::slug($this->title)
            ]);

            session()->flash('success', 'Post Created Successfully!!');
            $this->resetFields();
            $this->addPost = false;
        } catch (\Exception $ex) {
            session()->flash('error', 'Something goes wrong!!');
        }
    }

    /**
     * show existing post data in edit post form
     * @param mixed $id
     * @return void
     */
    public function edit($id)
    {
        try {
            $post = \App\Models\Post::findOrFail($id);
            if (!$post) {
                session()->flash('error', 'Post not found');
            } else {
                $this->title = $post->title;
                $this->content = $post->content;
                $this->status = $post->status;
                $this->postId = $post->id;
                $this->updatePost = true;
                $this->addPost = false;
            }
        } catch (\Exception $ex) {
            session()->flash('error', 'Something goes wrong!!');
        }

    }

    /**
     * update the post data
     * @return void
     */
    public function update()
    {
        $this->validate();
        try {
            \App\Models\Post::whereId($this->postId)->update([
                'title' => $this->title,
                'content' => $this->content,
                'status' => $this->status,
                'slug' => Str::slug($this->title)
            ]);
            session()->flash('success', 'Post Updated Successfully!!');
            $this->resetFields();
            $this->updatePost = false;
        } catch (\Exception $ex) {
            session()->flash('success', 'Something goes wrong!!');
        }
    }

    /**
     * Cancel Add/Edit form and redirect to post listing page
     * @return void
     */
    public function cancel()
    {
        $this->addPost = false;
        $this->updatePost = false;
        $this->resetFields();
    }

    /**
     * delete specific post data from the posts table
     * @param mixed $id
     * @return void
     */
    public function destroy($id)
    {
        try {
            \App\Models\Post::find($id)->delete();
            session()->flash('success', "Post Deleted Successfully!!");
        } catch (\Exception $e) {
            session()->flash('error', "Something goes wrong!!");
        }
    }
}

Once done, save the app/Http/Livewire/Post.php file again.

Next, let's create a new file resources/views/home.blade.php, then add the following lines of code.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Belajar Laravel 10 - Crud Laravel Livewire @ qadrlabs.com</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"
          integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">

    @livewireStyles
</head>

<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="/">Livewire</a>
    </div>
</nav>
<div class="container">
    <div class="row justify-content-center mt-3">
        @livewire('post')
    </div>
</div>

@livewireScripts
</body>

</html>


To display the list of posts, open the component file resources/views/livewire/post.blade.php, then adjust the content of the post.blade.php file to match the following code lines.

<div>
    <div class="col-md-12 mb-2">
        @if(session()->has('success'))
            <div class="alert alert-success" role="alert">
                {{ session()->get('success') }}
            </div>
        @endif

        @if(session()->has('error'))
            <div class="alert alert-danger" role="alert">
                {{ session()->get('error') }}
            </div>
        @endif

        @if($addPost)
            @include('livewire.create')
        @endif

        @if($updatePost)
            @include('livewire.update')
        @endif
    </div>

    <div class="col-md-12">
        <div class="card">
            <div class="card-header">
                @if(!$addPost)
                    <button wire:click="create()" class="btn btn-primary btn-sm float-end">Add New Post</button>
                @endif
            </div>
            <div class="card-body">

                <div class="table-responsive">
                    <table class="table">
                        <thead>
                        <tr>
                            <th>Name</th>
                            <th>Content</th>
                            <th>Status</th>
                            <th>Action</th>
                        </tr>
                        </thead>
                        <tbody>
                        @forelse ($posts as $post)
                            <tr>
                                <td>
                                    {{$post->title}}
                                </td>
                                <td>
                                    {{$post->content}}
                                </td>
                                <td>{{ $post->status == 1 ? 'Draft':'Publish' }}</td>
                                <td>
                                    <button wire:click="edit({{$post->id}})"
                                            class="btn btn-primary btn-sm">Edit</button>
                                    <button wire:click="destroy({{ $post->id }})"
                                            class="btn btn-danger btn-sm">Delete</button>
                                </td>
                            </tr>
                        @empty
                            <tr>
                                <td colspan="4" align="center">
                                    No Posts Found.
                                </td>
                            </tr>
                        @endforelse
                        </tbody>
                    </table>

                </div>
            </div>
        </div>
    </div>
</div>

Now let's create a new component file to display a form for adding data, named resources/views/livewire/create.blade.php. Then type the following code lines.

<div class="card">
    <div class="card-body">
        <form>
            <div class="form-group mb-3">
                <label for="title">Title:</label>
                <input type="text" class="form-control @error('title') is-invalid @enderror" id="title"
                       placeholder="Enter Title" wire:model="title">
                @error('title')
                <span class="text-danger">{{ $message }}</span>
                @enderror
            </div>
            <div class="form-group mb-3">
                <label for="content">content:</label>
                <textarea class="form-control @error('content') is-invalid @enderror" id="content"
                          wire:model="content" placeholder="Enter content"></textarea>
                @error('content')
                <span class="text-danger">{{ $message }}</span>
                @enderror
            </div>
            <div class="form-group mb-3">
                <label for="status">Status:</label>
                <select name="status" id="status" class="form-control @error('status') is-invalid @enderror" wire:model="status">

                    <option value="1">Draft</option>
                    <option value="2">Publish</option>
                </select>
                @error('status')
                <span class="text-danger">{{ $message }}</span>
                @enderror
            </div>
            <div class="d-grid gap-2">
                <button wire:click.prevent="store()" class="btn btn-success btn-block">Save</button>
                <button wire:click.prevent="cancel()" class="btn btn-secondary btn-block">Cancel</button>
            </div>
        </form>
    </div>
</div>


Let's create the second component file to handle the data update process, named resources/views/livewire/update.blade.php. Adjust it with the following code lines.

<div class="card">
    <div class="card-body">
        <form>
            <div class="form-group mb-3">
                <label for="title">Title:</label>
                <input type="text" class="form-control @error('title') is-invalid @enderror" id="title"
                       placeholder="Enter Title" wire:model="title">
                @error('title')
                <span class="text-danger">{{ $message }}</span>
                @enderror
            </div>
            <div class="form-group mb-3">
                <label for="content">content:</label>
                <textarea class="form-control @error('content') is-invalid @enderror" id="content"
                          wire:model="content" placeholder="Enter content"></textarea>
                @error('content')
                <span class="text-danger">{{ $message }}</span>
                @enderror
            </div>
            <div class="form-group mb-3">
                <label for="status">Status:</label>
                <select name="status" id="status" class="form-control @error('status') is-invalid @enderror" wire:model="status">
                    <option value="1">Draft</option>
                    <option value="2">Publish</option>
                </select>
                @error('status')
                <span class="text-danger">{{ $message }}</span>
                @enderror
            </div>
            <div class="d-grid gap-2">
                <button wire:click.prevent="update()" class="btn btn-success btn-block">Update</button>
                <button wire:click.prevent="cancel()" class="btn btn-secondary btn-block">Cancel</button>
            </div>
        </form>
    </div>
</div>


Step 6 - Define Route

Open the file routes/web.php, then define the route to display our project page.

Route::get('/', function () {
    return view('home');
});

Step 7 - Test Our Project

Next, let's run our project. Open the terminal again and run the following command:

php artisan serve

After that, open the link http://127.0.0.1:8000 in your browser and you can try adding, updating, and deleting data.

Conclusion

In this tutorial, we have successfully created a simple project with CRUD features using the Laravel Livewire package. After trying to add data, edit data, and delete data, you might have noticed a difference in the interface of our project and found it interesting to explore further. For instance, you could consider adding some common features like search, sorting, and pagination.

What do you think? Are you interested in giving it a try?