Laravel Livewire | Create Todo App with Filtering, Sorting and Pagination
In this tutorial, you’ll learn to create Todo Application with Filtering, Sorting and Pagination using Laravel Livewire Package.
By the end of this tutorial you’ll master below topic of Livewire:
- To validate form submitted data using Request class.
- Working with multiple adjacent components and passing data.
- Triggering notification on success or failure of the task.
- Using Listeners and calling other component listeners.
- Updating URL query string over the flow.
- Paginating the list data and add custom features to paginator.
- Filtering and Sorting objects list as per user actions.
- Loading objects list after component fully rendered.
Prerequisite
In this tutorial, some advance topics of livewire are considered and I have a developer recommend if you are new to laravel framework you can learn from its official site. And you know laravel very well and new to livewire package than you can visit livewire official site.
Complete code snippet with explanation
Create database migration for todo table using php artisan make:migration create_todo_table --create=todos
and at path 2020_05_15_174628_create_todo_table.php
class is created.
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateTodoTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('todos', function (Blueprint $table) { $table->bigIncrements('todo_id'); $table->string('title', 100); $table->longText('desc')->nullable(); $table->string('status')->default('pending'); $table->timestamps(); $table->softDeletes(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('todos'); } }
Create a model for Todo table using php artisan make:model TodoModel
.
<?php namespace App; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Log; class TodoModel extends Model { protected $table="todos"; protected $primaryKey = 'todo_id'; protected $fillable = ['title', 'desc', 'status']; }
In routes/web.php
.
// Livewire Todo Application Routes Route::get('todo', function(){ return view('livewire.todo.base', []); });
In resources/views/livewire/todo/base.blade.php
.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Laravel Livewire | Todo Application with Sorting, Filtering and Paginating</title> @livewireStyles @livewireScripts <link rel="stylesheet" href="{{ url('assets/css/bootstrap.min.css') }}"> <style> .error{ color: red; } .container{ max-width: 70%; } </style> </head> <body> <div class="container" > <div class="wrapper"> <div class="title-container"> <h1 class="title text-center">Laravel Livewire | Todo Application with Sorting, Filtering and Paginating</h1> </div> <div class="row"> <div class="col-md-3"> @livewire('todo.todo-notification-component') <!-- This component will show notification when todo is saved or updated --> @livewire('todo.form-component') <!-- This component will display Todo form --> </div> <div class="col-md-9"> @livewire('todo.list-component') <!-- This component will list Todos --> </div> </div> </div> </div> <script src="{{ url('assets/js/jquery.min.js') }}"></script> <script src="{{ url('assets/js/popper.min.js') }}"></script> <script src="{{ url('assets/js/bootstrap.min.js') }}"></script> </body> </html>
Navigating you route todo will render above blade file livewire components.
Here three components included in this template for full filling of below purpose.
- The todo.todo-notification-component: Displays any notification related to saving and updating of todo form.
- The todo.form-component: renders todo form and it also validates, saves, edit and updates todo.
- The todo.list-component: renders filter form elements, tables with a list of todos and custom pagination.
In app/Http/Livewire/Todo/TodoNotificationComponent.php
.
<?php namespace App\Http\Livewire\Todo; use Livewire\Component; use Log; class TodoNotificationComponent extends Component { public $listeners = [ "flash_message" => "flashMessage" ]; public function flashMessage($type, $msg){ session()->flash($type, $msg); } public function render() { return view('livewire.todo.todo-notification'); } }
Here $listeners
is an event listener which has flash_message as event and flashMessage as a method which will be triggered in response when flash_message is called using $this->emitTo('todo.todo-notification-component', 'flash_message', $arg1, $arg2);
.
In resources/views/livewire/todo/todo-notification.blade.php
.
<div> @if(session()->has('success')) <div class="alert alert-success alert-dismissible fade show"> <button type="button" class="close" data-dismiss="alert">×</button> {{ session('success') }} </div> @endif @if(session()->has('error')) <div class="alert alert-error alert-dismissible fade show"> <button type="button" class="close" data-dismiss="alert">×</button> {{ session('error') }} </div> @endif </div>
The method flashMessage
just adds a message to flash session. You’ll see notification text like this shown below.
In app/Http/Requests/TodoFormRequest.php
.
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class TodoFormRequest extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'todo_id' => 'nullable', 'title' => 'required', 'desc' => 'nullable', 'status' => 'required', ]; } public function messages() { return [ 'title.required' => 'Title is required.', 'status.required' => 'Status not selected', ]; } }
In app/Http/Livewire/Todo/FormComponent.php
.
<?php namespace App\Http\Livewire\Todo; use Livewire\Component; use Log; use App\Http\Requests\TodoFormRequest; use DB; use App\TodoModel; use Exception; class FormComponent extends Component { public $submit_btn_title = "Save Task"; public $form = [ "todo_id" => NULL, "title" => "", "desc" => "", "status" => "", ]; public $listeners = [ "edit" => "edit" ]; public function mount(){ } public function edit($todo_id){ try { $this->submit_btn_title = "Update Task"; $todo = TodoModel::find($todo_id); $this->form = $todo->toArray(); } catch (Exception $ex) { } } public function save(){ $form = new TodoFormRequest(); $form->merge($this->form); $validated_data = $form->validate($form->rules()); DB::beginTransaction(); try { $query = [ "title" => $validated_data["title"], "desc" => $validated_data["desc"], "status" => $validated_data["status"], ]; $condition = [ "todo_id" => $validated_data["todo_id"] ]; $info["todo"] = TodoModel::updateOrCreate($condition, $query); DB::commit(); $info['success'] = TRUE; } catch (\Exception $e) { DB::rollback(); $info['success'] = FALSE; } if($info["success"]){ $type = "success"; if($info["todo"]->wasRecentlyCreated){ $message = "New Task created successfully."; }else{ $message = "Task updated successfully."; } $this->submit_btn_title = "Save Task"; }else{ $type = "error"; $message = "Something went wrong while saving task."; } $this->emitTo('todo.todo-notification-component', 'flash_message', $type, $message); $this->emitTo('todo.list-component', 'load_list'); } public function render() { return view('livewire.todo.create_form'); } }
The FormComponent contains edit and save methods with the edit() the method as a listener. The property public $form
is an array and which is mapped to HTML form elements via wire:model directive.
In resources/views/livewire/todo/create_form.blade.php
.
<div class="form-container"> <form wire:submit.prevent="save" method="post"> <div class="row"> <div class="col-md-12"> <input type="hidden" wire:model="form.todo_id" > <label for="">Title</label> <input type="text" class="form-control" wire:model="form.title" > @error('title') <label class="error">{{ $message }}</label> @enderror </div> </div> <br> <div class="row"> <div class="col-md-12"> <label for="">Description</label> <textarea rows="3" class="form-control" wire:model="form.desc" ></textarea> @error('desc') <label class="error">{{ $message }}</label> @enderror </div> </div> <br> <div class="row"> <div class="col-md-12"> <label for="">Task Status</label> <select class="form-control" wire:model="form.status" > <option value="">Choose One</option> <option value="pending">Task Pending</option> <option value="accomplished">Task Accomplished</option> </select> @error('status') <label class="error">{{ $message }}</label> @enderror </div> </div> <br> <div class="row"> <div class="col-md-12"> <button type="submit" class="btn btn-primary btn-md" >{{ $submit_btn_title }}</button> </div> </div> </form> </div>
In app/Http/Livewire/Todo/ListComponent.php
.
<?php namespace App\Http\Livewire\Todo; use Livewire\Component; use Log; use App\TodoModel; class ListComponent extends Component { public $objects = []; public $paginator = []; public $page = 1; public $items_per_page = 5; public $loading_message = ""; public $listeners = [ "load_list" => "loadList" ]; public $filter = [ "search" => "", "status" => "", "order_field" => "", "order_type" => "", ]; protected $updatesQueryString = ['page']; public function mount(){ $this->loadList(); } public function loadList(){ $this->loading_message = "Loading Todos..."; $query = []; if(!empty($this->filter["status"])){ $query["status"] = $this->filter["status"]; } $objects = TodoModel::where($query); // Search if(!empty($this->filter["search"])){ $filter = $this->filter; $objects = $objects->where(function ($query) use ($filter) { $query->where('title', 'LIKE', $this->filter['search'] . '%'); }); } // Ordering if(!empty($this->filter["order_field"])){ $order_type = (!empty($this->filter["order_type"]))? $this->filter["order_type"]: 'ASC'; $objects = $objects->orderBy($this->filter["order_field"], $order_type); } // Paginating $objects = $objects->paginate($this->items_per_page); $this->paginator = $objects->toArray(); $this->objects = $objects->items(); } // Pagination Method public function applyPagination($action, $value, $options=[]){ if( $action == "previous_page" && $this->page > 1){ $this->page-=1; } if( $action == "next_page" ){ $this->page+=1; } if( $action == "page" ){ $this->page=$value; } $this->loadList(); } public function render() { return view('livewire.todo.list'); } }
The ListComponent handles displaying data into the table with custom pagination and also has a form of elements for displaying data as the user selects.
The properties $paginator, $page, $items_per_page, $updatesQueryString and the method applyPagination() perform manupulation for pagination.
Note
The property $updatesQueryString
is used by livewire package for dynamically modifying URL query string. It takes an array with variable names to append into the query string.
Example: Changing $page will automatically update ?page=
in URL.
The loadList() method performs filtering, sorting and pagination operations.
In resources/views/livewire/todo/list.blade.php
.
<div class="list-container"> <style> .table p{ margin: 0; } .filter-container{ margin-bottom: 15px; padding: 15px 10px; background: #ffc107; } .filter-container > .row{ margin: 0; } .filter-container > .row > div{ padding: 0 5px; } </style> <div wire:loading wire:init="loadList" > {{ $loading_message }} </div> <div class="filter-container"> <h2>Filter</h2> <div class="row"> <div class="col-md-3"> <label for="">Search Title</label> <input type="text" class="form-control" wire:model="filter.search" > </div> <div class="col-md-2"> <label for="">Status</label> <select wire:model="filter.status" class="form-control" > <option value="">Choose One</option> <option value="pending">Task Pending</option> <option value="accomplished">Task Accomplished</option> </select> </div> <div class="col-md-3"> <label for="">Order Field</label> <select wire:model="filter.order_field" class="form-control" > <option value="">Choose One</option> <option value="title">Task Title</option> <option value="status">Task Status</option> </select> </div> <div class="col-md-2"> <label for="">Order Type</label> <select wire:model="filter.order_type" class="form-control" > <option value="">Choose One</option> <option value="ASC">Ascending</option> <option value="DESC">Descending</option> </select> </div> <div class="col-md-2" style="display: flex;align-items: flex-end;" > <div> <button type="button" wire:click="loadList" class="btn btn-primary" >Filter</button> </div> </div> </div> </div> <div class="table-responsive"> <table class="table table-hover table-bordered"> <thead> <tr> <th style="width:50%;" >Title</th> <th>Status</th> <th>Action</th> </tr> </thead> <tbody> @if(!empty($objects)) @foreach($objects as $k => $v) <tr> <td> <div> <p><strong>Title:</strong> {{ $v["title"] }}</p> <p><strong>Description:</strong> {{ $v["desc"] }}</p> </div> </td> <td> @if($v["status"]=="pending") Pending @endif @if($v["status"]=="accomplished") Accomplished @endif </td> <td> <button type="button" class="btn btn-info" wire:click="$emitTo('todo.form-component', 'edit', {{ $v['todo_id'] }})" >Edit</button> <button type="button" class="btn btn-danger" >Remove</button> </td> </tr> @endforeach @else <tr> <td colspan="4" class="text-center" >No Tasks found.</td> </tr> @endif </tbody> </table> </div> <div class="pagination-container"> <ul class="pagination"> <li class="page-item @if($page == 1) disabled @endif "> <a class="page-link" href="javascript:void(0)" wire:click="applyPagination('previous_page', {{ $page-1 }})" > Previous </a> </li> <li class="page-item @if($page == $paginator['last_page']) disabled @endif "> <a class="page-link" href="javascript:void(0)" @if($page <= $paginator['last_page']) wire:click="applyPagination('next_page', {{ $page+1 }})" @endif > Next </a> </li> <li class="page-item" style="margin: 0 5px" > Jump to Page </li> <li class="page-item" style="margin: 0 5px" > <select class="form-control" title="" style="width: 80px" wire:model="page" > @for($i=1;$i<=$paginator['last_page'];$i++) <option value="{{ $i }}">{{ $i }}</option> @endfor </select> </li> <li class="page-item" style="margin: 0 5px" > Items Per Page </li> <li class="page-item" style="margin: 0 5px" > <select class="form-control" title="" style="width: 80px" wire:model="items_per_page" wire:change="loadList" > <option value="5">05</option> <option value="10">10</option> <option value="20">20</option> <option value="30">30</option> </select> </li> </ul> </div> </div>
This template contains HTML elements with livewire directives for displaying the list.
Result
Video Output
Conclusion
In conclusion, this was the end of the tutorial on Laravel Livewire | Todo App with Filtering, Sorting and Pagination. Comment if you have doubts we’ll reach you soon and also help us grow by sharing this post.
Related Posts
- Laravel Livewire | Multiple Files Upload with Validation
- Login & Dynamic Registration Form for Multiple Roles using Laravel Livewire




