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.
Laravel Livewire triggering or emmiting flash message for todo saved successfully.

Laravel Livewire triggering or emmiting flash message for todo failed to save.

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

Laravel Livewire | Todo App with Filtering, Sorting and Paginating output preview

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

Summary
Review Date
Reviewed Item
Laravel Livewire | Create Todo App with Filtering, Sorting and Pagination
Author Rating
51star1star1star1star1star
Software Name
Laravel Livewire
Software Name
ubuntu, windows, mac os
Software Category
web development