Build a Simple CRUD Todo App with Laravel and Livewire

Chukwuyenum Opone
15 min readMar 20, 2023
Build Todo App Header

Are you looking to develop a solid foundation for building CRUD web applications in any language or framework? Look no further than this tutorial! I’ll be documenting an old project of mine, a to-do application, that I never got around to documenting during my learning stage. Using Laravel and Livewire, I’ll guide you through each step of the process to create a simple yet powerful to-do app.

By the end of this tutorial, you’ll have a fully functional to-do app under your belt and a solid understanding of how to develop CRUD applications using Laravel and Livewire. Let’s get started!

Technology

To build our to-do application, I will be utilizing a variety of technologies to ensure a smooth and efficient development process. The main framework I’ll be using is Laravel, which is a free, open-source PHP web framework. Additionally, I’ll be integrating Livewire, a full-stack framework for Laravel that allows for the creation of dynamic, reactive interfaces.

To manage our development environment, we’ll be using Docker Desktop, which provides a platform for easily creating, deploying, and running applications. Node and Npm will also be installed, which will allow us to manage our application’s frontend dependencies.

Bootstrap, a popular CSS framework, will be used to enhance the look and feel of our application. We’ll also be utilizing Sail, a lightweight command-line interface that streamlines the use of Laravel with Docker, to handle deployment.

Getting Started

To get started, ensure that you have Laravel, Docker Desktop, Node, and Npm installed on your Mac. Don’t worry about installing Sail and Bootstrap just yet, as these will be installed within the application itself. With these technologies in place, we can begin building our powerful to-do app with ease!

I will be developing on a Mac and Docker Desktop is already installed and running, you can use a simple terminal command to create a new Laravel project. For example, to create a new Laravel application in a directory named “todo-app”, you may run the following command in your terminal:

curl -s "https://laravel.build/todo-app" | bash

After your Laravel project builds completely you can start it using the next command

Setup Docker With Sail

//To Navigate to project directory
cd todo-app

//To configure a shell alias that allows you to execute Sail's commands more easily
alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail'

//To migrate database and seed the Pre-defined Factory data into it
sail php artisan migrate:fresh --seed

//To bootstrap vite for frontend html, css and js.
sail npm run dev

//To run in Background
sail up -d

// To Stop Docker
sail stop

Scaffolding my project UI with bootstrap

//To install Laravel UI package before
composer require laravel/ui
//To install Boostrap 5 Auth Scaffolding
sail php artisan ui bootstrap --auth
//To install node modules, compile and start frontend using vite
sail npm install && sail npm run dev

After Executing the above codes successfully it should setup your project to use bootstrap and create auth pages, controllers, and routes for the following:

Login, Register, Forgot Password, Dashboard, and Welcome

For this project, I want only logged-in users to be able to see their task list so I would delete the welcome page as it is not auth protected and leave the Dashboard as this would house the todo livewire components I will be creating next.

Next Step setting up livewire

//To Install Livewire on your project
composer require livewire/livewire

Include Livewire Styles and Script (on layouts/app page).

//layouts/app.blade.php
...

<!-- Scripts -->
@vite(['resources/sass/app.scss', 'resources/js/app.js'])
@livewireStyles()
</head>
<body>
<div id="app">
<main class="py-4">
@yield('content')
</main>
</div>

@livewireScripts()
@yield('scripts')
</body>

Next, I will be adding third-party packages like Turbolinks, Toaster, and feather.js to be used in the project

Turbolinks: Livewire recommends you use Turbolinks in your apps to make page transitions faster. It is very possible to achieve a “SPA” feeling application written with Turbolinks & Livewire.

NOTE: Livewire no longer supports Turbolinks out of the box.

If you want to continue using Turbolinks in your Livewire application, you will have to include the Turbolinks adapter alongside Livewire’s JavaScript assets:

//layouts/app.blade.php
...
<script src="https://cdn.jsdelivr.net/gh/livewire/turbolinks@v0.1.x/dist/livewire-turbolinks.js" data-turbolinks-eval="false"></script>
@livewireScripts
@yield('scripts')
</body>

Toaster: ToastrJS is a JavaScript library for Gnome / Growl-type non-blocking notifications. jQuery is required. The goal is to create a simple core library that can be customized and extended.

Feather: Feather is a collection of simply beautiful open-source icons. Each icon is designed on a 24x24 grid with an emphasis on simplicity, consistency, and flexibility.

//layouts/app.blade.php

...
<link href="https://fonts.bunny.net/css?family=Nunito" rel="stylesheet">

<!-- Scripts -->
@vite(['resources/sass/app.scss', 'resources/js/app.js'])
<link href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.0.1/css/toastr.css" rel="stylesheet"/>

@livewireStyles()
</head>
<body>
...

<script src="https://code.jquery.com/jquery-3.6.3.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/livewire/turbolinks@v0.1.x/dist/livewire-turbolinks.js" data-turbolinks-eval="false"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.0.1/js/toastr.js"></script>

@livewireScripts
@yield('scripts')
<script>
window.addEventListener('toast-success', event => {
console.log('toast-succcess');
// Display a success toast, with a message
toastr.success(event.detail.message, {timeOut: 10000})
});
window.addEventListener('toast-message', event => {
console.log('toast-message');
// Display a warning toast, with a message
toastr.warning(event.detail.message, {timeOut: 10000})

});
window.addEventListener('toast-error', event => {
console.log('toast-error');
// Display an error toast, with a message
toastr.error(event.detail.message, {timeOut: 10000})
})
</script>
</body>

The next step is to configure our Login page

php artisan make:livewire Login

This creates two files :

  • app/Http/Livewire/Login.php
  • resources/views/livewire/login.blade.php

We go further to edit three files relating to the login

resources/views/auth/login.blade.php

@extends('layouts.app')

@section('content')
<livewire:login/>
@endsection

resources/views/livewire/login.blade.php

<div class="container">
<div class="row justify-content-center">
<div class="col-md-12">
<h1 class="my-5 text-center display-3">{{ __('Login')}}</h1>
</div>

<div class="col-md-6 mx-auto">
<form wire:submit.prevent="handleSubmit">
@csrf
<div class="form-group my-2">
<input
id="email"
type="email"
class="form-control @error('email') is-invalid @enderror"
value="{{ old('email') }}"
autocomplete="name"
autofocus placeholder="Email"
wire:model="email"/>

@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>

<div class="form-group my-2">
<input id="password"
type="password"
class="form-control @error('password') is-invalid @enderror"
autocomplete="new-password"
placeholder="Password"
wire:model="password"/>

@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-block btn-dark">
{{ __('Login') }}
</button>
</div>
</form>
</div>
</div>
</div>

app/http/livewire/Login.php

<?php

namespace App\Http\Livewire;

use App\Models\User;
use Livewire\Component;
use Illuminate\Support\Facades\Auth;

class Login extends Component
{
public $email;

public $password;

public $error;

protected $rules = [
'email' => 'required|email',
'password' => 'required|min:8'
];

public function render()
{
return view('livewire.login');
}

public function handleSubmit()
{
$this->validate();
if(Auth::attempt(['email' => $this->email, 'password' => $this->password])){
//flash success message with toaster
$this->dispatchBrowserEvent('toast-success', ['message' => 'You are logged in successfully']);
return redirect(route('home'));
}else{
$this->email = '';
$this->password = '';
return $this->error = "Invalid Credentials";
//flash error message with toaster
$this->dispatchBrowserEvent('toast-error', ['message' => $this->error]);
}
}
}

Then we go on to configure our Register page

php artisan make:livewire Register

This creates two files :

  • app/Http/Livewire/Register.php
  • resources/views/livewire/register.blade.php

We go further to edit three files relating to register

resources/views/auth/register.blade.php

@extends('layouts.app')

@section('content')
<livewire:register/>
@endsection

resources/views/livewire/register.blade.php

<div class="container">
<div class="row justify-content-center">
<div class="col-md-12">
<h1 class="my-5 text-center display-3">{{ __('Register')}}</h1>
</div>

<div class="col-md-6 mx-auto">
<form wire:submit.prevent="handleSubmit">
@csrf
<div class="form-group my-2">
<input
type="text"
class="form-control @error('first_name') is-invalid @enderror"
value="{{ old('first_name') }}"
placeholder="First Name"
wire:model="first_name"/>

@error('first_name')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>

<div class="form-group my-2">
<input
type="text"
class="form-control @error('last_name') is-invalid @enderror"
value="{{ old('last_name') }}"
placeholder="Last Name"
wire:model="last_name"/>

@error('last_name')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>

<div class="form-group my-2">
<input
type="email"
class="form-control @error('email') is-invalid @enderror"
value="{{ old('email') }}"
placeholder="Email"
wire:model="email"/>

@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>

<div class="form-group my-2">
<input
type="password"
class="form-control @error('password') is-invalid @enderror"
placeholder="Password"
wire:model="password"/>

@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>

<div class="form-group my-2">
<input
type="password"
class="form-control @error('confirmPass') is-invalid @enderror"
placeholder="Confirm Password"
wire:model="confirmPass"/>
@error('confirmPass')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-block btn-dark">
{{ __('Register') }}
</button>
</div>
</form>
</div>
</div>
</div>

app/http/livewire/Register.php

<?php


namespace App\Http\Livewire;

use App\Models\User;
use Livewire\Component;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Hash;

class Register extends Component
{
// declare form input variables
public $first_name;
public $last_name;
public $email;
public $password;
public $confirmPass;
public $error;
//set form validation rules
protected $rules = [
'first_name' => 'required|min:3',
'last_name' => 'required|min:3',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|same:confirmPass',
'confirmPass' => 'required'
];

public function render()
{
return view('livewire.register');
}

public function handleSubmit()
{
//validation
$this->validate();

//hash password
$hashPass = Hash::make($this->password);

//register user
$user = User::create([
'first_name' => $this->first_name,
'last_name' => $this->last_name,
'email' => $this->email,
'password' => $hashPass
]);

if($user){
//flash success message
$this->dispatchBrowserEvent('toast-success', ['message' => 'You are Registered Successfully']);
//if user successfully created redirect to login page
return redirect(route("login"));
}
// else return error
$this->error = "Something went wrong";
$this->dispatchBrowserEvent('toast-error', ['message' => $this->error]);
return;
}
}

Now that the Auth is set we can now move to the main Focus Todo:

We create a Todo Model, Migration File, Controller, and Dashboard LiveComponents

php artisan make:model Todo -mcfs

This creates a Todo Model, migration, controller, factory, and seeder files

  • app/Models/Todo.php
<?php

namespace App\Models;

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

class Todo extends Model
{
use HasFactory;
use SoftDeletes;

const PENDING = 0;
const ONGOING = 1;
const COMPLETED = 2;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'description',
'status',
'completed_at'
];

public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}
  • database/migrations/2023_03_12_165342_create_todos_table.php
<?php

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

return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('todos', function (Blueprint $table) {
$table->id();
$table->string('description');
$table->integer('status')->default(0);
$table->timestamp('completed_at')->nullable();
$table->unsignedBigInteger('user_id');
$table->softDeletes();
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('todos');
}
};
  • app/Http/Controllers/TodoController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TodoController extends Controller
{
//
}
  • database/factories/TodoFactory.php
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Todo>
*/
class TodoFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
$status = random_int(0,2);
return [
'description' => fake()->sentence(3),
'status' => $status,
'completed_at' => $status == 2 ? now() : null,
'user_id' => 1,
];
}
}
  • database/seeders/TodoSeeder.php
<?php

namespace Database\Seeders;

use App\Models\Todo;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class TodoSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
//Truncate Todo
Todo::truncate();

//Create 10 Default Todo with factory
$todos = Todo::factory()->count(10)->create();
}
}

Now that I have set up my Todo I need to go and modify my users to link with the todo I will be seeding to the database

  • app/Models/User.php
<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;

/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'first_name',
'last_name',
'email',
'password'
];

/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];

/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];

public function tasks()
{
return $this->hasMany(Todo::class, 'user_id');
}

public function pendingTasks()
{
return $this->tasks->where('status','',Todo::PENDING)
->orderBy('created_at', 'desc')
->get();
}

public function ongoingTasks()
{
return $this->tasks->where('status','',Todo::ONGOING)
->orderBy('created_at', 'desc')
->get();
}

public function completedTasks()
{
return $this->tasks->where('completed_at', '!=', null)
->orderBy('completed_at', 'desc')
->get();
}

}
  • database/factories/UserFactory.php
<?php

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
*/
class UserFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'first_name' => fake()->firstName(),
'last_name' => fake()->lastName(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
];
}

/**
* Indicate that the model's email address should be unverified.
*
* @return static
*/
public function unverified()
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}
  • database/seeders/UserSeeder.php
<?php

namespace Database\Seeders;

use App\Models\User;
use Illuminate\Support\Str;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;

class UserSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
//Truncate User
User::truncate();

//hash password
$hashPass = Hash::make('password');

//Seed Author User
$user = User::factory()->create([
'first_name' => 'Test',
'last_name' => 'User',
'email' => 'test@user.com',
'password' => $hashPass
]);

//Create 5 Default Users with factory
$users = User::factory()->count(5)->create();
}
}

You set your database seeder call in the database seeder file

  • database/seeders/DatabaseSeeder.php
<?php

namespace Database\Seeders;

// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
//This calls the seeder classes and runs them
$this->call([
UserSeeder::class,
TodoSeeder::class
]);
}
}

Now that we are done with setting up the User and Todo model, factory, seeders, and One->To->Many Relationship.

We move on to the next step, where we build a Home Page blade file, and Home Page Component, specify routes in “routes/web.php” and implement the Todo Business Logic in the Home component

The next step is to configure our Home page

php artisan make:livewire Home

This creates two files :

  • app/Http/Livewire/Home.php
  • resources/views/livewire/home.blade.php

We go further to edit seven files relating to the home page

Create A CheckAuth Middleware with the command bellow

sail php artisan make:middleware CheckAuth

app/Http/Middleware/CheckAuth.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class CheckAuth
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
if (Auth::user()) {
// if user is logged in already redirect them to home page
return redirect(route('home'));
}
return $next($request);
}
}

appHttp/Kernel.php

    ...
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array<string, class-string|string>
*/
protected $routeMiddleware = [
...
'checkAuth' => \App\Http\Middleware\CheckAuth::class,
...
];
...

routes/web.php

<?php
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\RegisterController;
use App\Http\Controllers\HomeController;
use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
// UnAuthenticated Routes
Route::group(['middleware' => 'checkAuth'], function(){
// Authentication Route : Login
Route::get('/login', [LoginController::class, 'login'])->name('login');
// Authentication Route : Register
Route::get('/register', [RegisterController::class,'register'])->name('register');
});
//Open Routes
// Authenticated Routes
Route::middleware('auth')->group(function (){
// Home Route
Route::get('/', [HomeController::class, 'index'])->name('home');
});

app/Http/Controllers/HomeController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class HomeController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}

/**
* Show the application dashboard.
*
* @return \Illuminate\Contracts\Support\Renderable
*/
public function index()
{
return view('home');
}
}

resources/views/home.blade.php

@extends('layouts.app')
@section('content')
<livewire:home/>
@endsection

resources/views/livewire/home.blade.php

    
<section class="vh-100" style="background-color: #eee;">
<div class="container py-5 h-100">
<div class="row d-flex justify-content-center align-items-center h-100">
<div class="col col-lg-9 col-xl-7">
<div class="card rounded-3">
<div class="card-body p-4">

<h4 class="text-center my-3 pb-3">To Do App</h4>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<ul>
<button wire:click="setStatus(5)" class="btn btn-sm mx-2 btn-outline-secondary fst-italic bold">
All Tasks
</button>
<button wire:click="setStatus(0)" class="btn btn-sm mx-2 btn-outline-danger fst-italic bold">
Pending
</button>
<button wire:click="setStatus(1)" class="btn btn-sm mx-2 btn-outline-info fst-italic bold">
Ongoing
</button>
<button wire:click="setStatus(2)" class="btn btn-sm mx-2 btn-outline-success fst-italic bold">
Completed
</button>
<button wire:click="setStatus(3)" class="btn btn-sm mx-2 btn-outline-dark fst-italic bold">
Trashed
</button>
</ul>
<div class="btn-toolbar mb-4 mb-md-0">
<div class="btn-group me-2">
<input
type="text"
class="form-control float-end mx-2"
value="{{ old('search') }}"
placeholder="Search"
wire:model="search"/>
</div>
</div>
</div>

<form wire:submit.prevent="storeTodo" class="row row-cols-lg-auto g-3 justify-content-center align-items-center mb-4 pb-2">
@csrf
<div class="col-12">

<div class="form-outline">
<input
type="text"
class="form-control float-end mx-2 @error('description') is-invalid @enderror"
value="{{ old('description') }}"
placeholder="Enter a task here"
wire:model="description"/>
@error('description')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>

<div class="col-12">
<button type="submit" class="btn btn-success">Create</button>
</div>
</form>

<table class="table mb-4">
<thead>
<tr>
<th scope="col">No.</th>
<th scope="col">Todo item</th>
<th scope="col">Status</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
@forelse ($todos as $key => $todo)
<tr>
<th scope="row">{{$key + 1}}</th>
<td>{{$todo->description}}</td>
<td>
@if ($todo->status == 0)
<span class="badge bg-primary">{{$todo->getStatus()}}</span>
@elseif ($todo->status == 1)
<span class="badge bg-secondary">{{$todo->getStatus()}}</span>
@else
<span class="badge bg-success">{{$todo->getStatus()}}</span>
@endif
</td>
<td>
@if ($todo->status == 0)
<button wire:click="markAsOngoing({{ $todo->id }})" type="button" class="btn btn-secondary btn-sm ms-1">
<span class="align-text-center text-white text-lg"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>
</button>
@elseif ($todo->status == 1)
<button wire:click="markAsPending({{ $todo->id }})" type="button" class="btn btn-primary btn-sm text-sm">
<span class="align-text-center text-white text-lg"><i class="fa fa-arrow-down" aria-hidden="true"></i></span>
</button>
<button wire:click="markAsCompleted({{ $todo->id }})" type="button" class="btn btn-success btn-sm ms-1 text-sm">
<span class="align-text-center text-white text-lg"><i class="fa fa-arrow-up" aria-hidden="true"></i></span>
</button>
@else
<button wire:click="markAsOngoing({{ $todo->id }})" type="button" class="btn btn-secondary btn-sm text-sm">
<span class="align-text-center text-white text-lg"><i class="fa fa-arrow-down" aria-hidden="true"></i></span>
</button>
@endif
@if (!$todo->trashed())
<button wire:click="trashTodo({{ $todo->id }})" type="button" class="btn btn-danger btn-sm text-sm">
<span><i class="fa fa-trash" aria-hidden="true"></i></span>
{{-- <span data-feather="trash-2" class="align-text-center text-white text-lg"></span> --}}
</button>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="7">
<p class="text-center">
No Todo Found
</p>
</td>
</tr>
@endforelse

{{ $todos->links() }}
</tbody>
</table>

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


app/http/livewire/Home.php

<?php

namespace App\Http\Livewire;

use App\Models\Tag;
use App\Models\Post;
use Livewire\Component;
use App\Models\Category;
use App\Models\Todo;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Str;
use Livewire\WithFileUploads;
use Livewire\WithPagination;

class Home extends Component
{
use WithPagination;
use WithFileUploads;

protected $paginationTheme = 'bootstrap';

public $description;

protected $rules = [
'description' => 'required'
];


protected $messages = [
'description.required' => 'The Description Field cannot be empty.',
];

public $todo;
public $status = 4;
protected LengthAwarePaginator $todos;

//set form validation rules
public $search = '';

public function updatingSearch()
{
$this->searchTodos();
}

public function render()
{
switch ($this->status) {
case Todo::PENDING:
$this->getPendingTodos();
break;
case Todo::ONGOING:
$this->getOngoingTodos();
break;
case Todo::COMPLETED:
$this->getCompletedTodos();
break;
case 3:
$this->getTrashedTodos();
break;
default:
$this->getTodos();
break;
}
return view('livewire.home',[
'todos' => $this->todos
]);
}

/**
* The attributes that are mass assignable.
*
* @var array
*/
private function resetInputFields(){
$this->description = '';
}

public function storeTodo()
{
$this->validate();

if (!$this->description) {
$this->dispatchBrowserEvent('toast-error', ['message' => 'Description is empty']);
return;
}

$task = $this->createTask();

if($task){
//flash success message
$this->dispatchBrowserEvent('toast-success', ['message' => 'You Successfully Created a Task']);
}else{
$this->dispatchBrowserEvent('toast-error', ['message' => 'Something went wrong!!!']);
}
$this->resetInputFields();
$this->getTodos();
}

public function markAsPending($id)
{
$this->getTodo($id);
if($this->todo){
$this->todo->status = Todo::PENDING;
if ($this->todo->completed_at) {
$this->todo->completed_at = null;
}
$this->todo->save();
}
$this->resetInputFields();
$this->getTodos();
}

public function markAsOngoing($id)
{
$this->getTodo($id);
if($this->todo){
$this->todo->status = Todo::ONGOING;
if ($this->todo->completed_at) {
$this->todo->completed_at = null;
}
$this->todo->save();
}
$this->resetInputFields();
$this->getTodos();
}

public function markAsCompleted($id)
{
$this->getTodo($id);
if($this->todo){
$this->todo->status = Todo::COMPLETED;
$this->todo->completed_at = now();
$this->todo->save();
}
$this->resetInputFields();
$this->getTodos();
}

public function trashTodo($id)
{
$this->getTodo($id);
if($this->todo){
$this->todo->delete();
}
$this->resetInputFields();
$this->getTodos();
}

private function createTask()
{
$todo = new Todo();
$todo->description = $this->description;
$todo->user_id = auth()->user()->id;
$todo->save();

return $todo;
}

private function getTodo($id)
{

$this->todo = Todo::where('user_id','=',auth()->user()->id)->where('id', '=', $id)
->first();
}

public function setStatus($value)
{
$this->status = $value;
}

private function getTodos()
{
$this->todos = Todo::where('user_id','=',auth()->user()->id)->paginate(10);
}

private function getPendingTodos()
{
$this->todos = Todo::where('user_id','=',auth()->user()->id)->where('status', '=', Todo::PENDING)->paginate(10);
}

private function getOngoingTodos()
{
$this->todos = Todo::where('user_id','=',auth()->user()->id)->where('status', '=', Todo::ONGOING)->paginate(10);
}

private function getCompletedTodos()
{
$this->todos = Todo::where('user_id','=',auth()->user()->id)->where('completed_at', '!=', null)->paginate(10);
}

private function getTrashedTodos()
{
$this->todos = Todo::onlyTrashed()->where('user_id','=',auth()->user()->id)->paginate(10);
}



/**
* The attributes that are mass assignable.
*
* @var array
*/
public function cancel()
{
$this->resetInputFields();
}

/**
* The attributes that are mass assignable.
*
* @var array
*/
public function update()
{
//set form validation rules
$this->validate([
'title' => 'required|min:3',
'body' => 'required|min:3',
]);

session()->flash('message', 'Task Updated Successfully.');
$this->resetInputFields();
$this->dispatchBrowserEvent('close-modal');
}

/**
* The attributes that are mass assignable.
*
* @var array
*/
public function delete()
{
session()->flash('message', 'Task Deleted Successfully.');
}
}

You can now run your project using the docker command

sail up -d

If your project is built successfully set up your database in .env file to connect with the docker sail database

# SAIL DB
# DB_CONNECTION=mysql
# DB_HOST=mysql
# DB_PORT=3306
# DB_DATABASE=todo_app
# DB_USERNAME=sail
# DB_PASSWORD=password

Then run the following commands in bash migrate and seed your database using the factory and seeder files

sail php artisan migrate:fresh --seed

Compile the CSS and javascript file by running the following command

npm install && npm run dev
Login Page
Register Page
Todo Home Page

By now, you should have a solid understanding of how to build a to-do app with Laravel and Livewire.

The app you’ve created can be extended to fit your needs and help you stay organized. With the power of Livewire, you can add more dynamic features and make your app more interactive. We hope this tutorial has been helpful to you and wish you the best of luck with your future Laravel and Livewire projects!

If you found this helpful please let me know what you think in the comments, You are welcome to suggest changes.

Thank you for the time spent reading this article.

You can follow me on all my social handles @officialyenum and please subscribe and 👏 would mean a lot thanks

--

--

Chukwuyenum Opone

A software and game programmer with over 5 years in tech and gaming, driven by innovation and a passion for AI, and a way with words.