Laravel 10 CRUD app using Livewire (TALL stack)

In this tutorial we’ll create Laravel 10 CRUD app using Livewire (TALL – Tailwind Alpine Laravel Livewire – stack). Laravel 10.x requires a minimum PHP version of 8.1. This tutorial assumes that you are using Linux, macOS or WSL on Windows and Node.js, Composer and PHP 8.1 along with required modules i.e bcmatch, sqlite, mbstring, xml, zip, gd, mcrypt, curl are properly installed. Also make sure that SQLite is installed as we’ll be using SQLite to keep things simple and quick; you may use MySQL or PostgreSQL as you like, however make sure the corresponding PHP module is installed too.

Using Composer, enter following command in terminal to install Laravel 10:

composer create-project laravel/laravel app 10.*

Go inside app folder:

cd app

If you want to open the project in VSCode, enter following command:

code .

Add Jetstream package by executing following command in project’s parent folder:

composer require laravel/jetstream:*

Then, install Jetstream with TALL based scaffolding by executing following command:

php artisan jetstream:install livewire

Next, setup database and Laravel environment file. Here, we’re using SQLite database for simplicity purposes. Being inside parent folder (app), execute following command to create new SQLite database file:

touch database/database.sqlite

Then find .env config file in the parent folder, open it and change DB_CONNECTION line as follows:

DB_CONNECTION = sqlite

Remove DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD from .env since we’re using SQLite database.

Initial setup of Laravel app is done! we can now start building the CRUD. First, enter the following command to create Post model along with the migration:

php artisan make:model Post -m

Open database/migrations/(date_stamp)_create_posts_table.php and then insert the desired table fields:

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('body');
            $table->timestamps();
        });
    }
    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

Enter following command to populate the database with tables:

php artisan migrate

Next, in order to enable mass-assignment for relevant data fields, open app/Models/Post.php model file and insert protected fillable array as follows:

<?php

namespace App\Models;

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

class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'title', 'body'
    ];
}

Then, create a Livewire component Posts by executing:

php artisan make:livewire Posts

The above command will create two files: first, app/Http/Livewire/Posts.php and second, resources/views/livewire/posts.blade.php.

Let’s insert following code inside app/Http/Livewire/Posts.php:

<?php

namespace App\Http\Livewire;

use Livewire\Component;
use App\Models\Post;

class Posts extends Component
{
    public $posts, $title, $body, $post_id;
    public $isOpen = false;

    public function render()
    {
        $this->posts = Post::all();
        return view('livewire.posts');
    }

    public function create()
    {
        $this->resetForm();
        $this->openModal();
    }

    public function openModal()
    {
        $this->isOpen = true;
    }

    public function closeModal()
    {
        $this->isOpen = false;
        $this->resetForm();
    }

    private function resetForm(){
        $this->post_id = '';
        $this->title = '';
        $this->body = '';
        $this->resetErrorBag();
    }
    
    public function store()
    {
        $this->validate([
            'title' => 'required',
            'body' => 'required',
        ]);
    
        Post::updateOrCreate(['id' => $this->post_id], [
            'title' => $this->title,
            'body' => $this->body,
        ]);

        session()->flash('message', $this->post_id ? 'Post updated!' : 'Post created!');

        $this->closeModal();
        $this->resetForm();
    }

    public function edit($id)
    {
        $post = Post::findOrFail($id);
        $this->post_id = $id;
        $this->title = $post->title;
        $this->body = $post->body;
    
        $this->openModal();
    }
    
    public function delete($id)
    {
        Post::find($id)->delete();
        session()->flash('message', 'Post deleted!');
    }    
}

Then, open resources/views/livewire/posts.blade.php and insert following code:

<x-slot name="header">
    <h2 class="font-semibold text-xl text-gray-800 leading-tight">
        {{ __('Posts') }}
    </h2>
</x-slot>
<div class="py-6">
    <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
        <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg px-4 py-4">
            @if (session()->has('message'))
            <div
            x-data="{ show: true }"
            x-show="show" 
            x-transition:leave="transition ease-in duration-300"
            x-transition:leave-start="opacity-100 transform scale-100"
            x-transition:leave-end="opacity-0 transform scale-90"
            class="bg-indigo-100 border-indigo-500 text-indigo-900 shadow-md mb-4 px-4 py-2"
            role="alert">
                <div class="flex justify-between">
                    <div>
                        <p class="text-sm">{{ session('message') }}</p>
                    </div>
                    <button @click="show = false" class="inline-flex font-extrabold text-indigo-900 focus:outline-none focus:text-indigo-500 transition ease-in-out duration-150">
                        &emsp; x
                    </button>
                </div>
            </div>
            @endif
            <x-button wire:click="create()" class="mb-4">Add Post</x-button>
            <x-modal wire:model.defer="isOpen">
            @include('livewire.posts-form')
            </x-modal>
            <table class="table-fixed w-full">
                <thead>
                    <tr class="bg-gray-400 text-white">
                        <th class="px-4 py-2 w-20">No.</th>
                        <th class="px-4 py-2">Title</th>
                        <th class="px-4 py-2">Body</th>
                        <th class="px-4 py-2">Action</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach($posts as $item)
                    <tr>
                        <td class="border px-4 py-2">{{ $item->id }}</td>
                        <td class="border px-4 py-2">{{ $item->title }}</td>
                        <td class="border px-4 py-2">{{ $item->body }}</td>
                        <td class="border px-4 py-2">
                            <x-secondary-button wire:click="edit({{ $item->id }})" class="mr-2">
                                Edit
                            </x-secondary-button>
                            <x-danger-button wire:click="delete({{ $item->id }})">
                                Delete
                            </x-danger-button>
                        </td>
                    </tr>
                    @endforeach
                </tbody>
            </table>
        </div>
    </div>
</div>

Now, create a new file posts-form.blade.php inside resources/views/livewire folder to serve as modal form:

touch resources/views/livewire/posts-form.blade.php

And then, insert following code inside resources/views/livewire/posts-form.blade.php:

<x-validation-errors class="m-4" />
<form>
    <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
        <x-input type="text" class="mb-4 w-full" placeholder="Title" wire:model="title" />
        <x-input type="text" class="mb-4 w-full" placeholder="Body" wire:model="body" />
    </div>
    <div class="ml-6 mb-4">
        <x-button wire:click.prevent="store()" type="button" class="mr-2">Save</x-button>
        <x-secondary-button wire:click="closeModal()" type="button">Cancel</x-secondary-button>
    </div>
</form>

Finally, open the routes/web.php and amend it as follows:

//...
use App\Http\Livewire\Posts;
//...
Route::get('posts', Posts::class)->middleware('auth')->name('posts');

Compile client side assets (JS and CSS files) by executing following command:

npm run build

Run PHP dev server by executing following command:

php artisan serv

Enter the following URL in your browser to access Posts Livewire component (don’t forget to create a user login first):

localhost:8000/posts

Leave a Comment