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">
  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