Build a to-do list app with Vue.js and Laravel

In this Laravel and Vue JS tutorial, we will explain how to build a to-do list app with Vue.js and Laravel.

A to-do list is a list of things you have to-do. So there will be anything and everything on your to-do list. The to-do list is created and tracked when your work is due and can help to prioritize and get work done.

So here in this tutorial, we will create to-do application using Laravel and Vue.js. We will add functionality to add items, list items, edit items and delete items.

Now let’s get started!


Create MySQL Database Table

We will create MySQL database todo_app and create table todo_items to store todos items.

CREATE TABLE `todo_items` (
  `id` bigint(20) UNSIGNED NOT NULL,
  `uuid` char(36) COLLATE utf8mb4_unicode_ci NOT NULL,
  `name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
  `deleted_at` timestamp NULL DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

ALTER TABLE `todo_items`
  ADD PRIMARY KEY (`id`);

ALTER TABLE `todo_items`
  MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=12;

Create To-Do Laravel Project

First we will create Laravel project and run below command to create Laravel proect laravel-vuejs-todolist.

composer create-project laravel/laravel laravel-vuejs-todolist

Scaffolding Vue.js

Now we wil do scaffolding of our project by installing Laravel and Vue packages.

Install laravel/ui package: We will install Laravel/ui package using below command.

composer require laravel/ui

Install the frontend scaffolding: We will install the frontend scaffolding using the below command. This will also generate auth scaffolding.


php artisan ui vue

Compile scaffolding: We will run below command to compile our fresh scaffolding.

npm install && npm run dev

Configure MySQL Connection Details

We will open .env file and update MySQL database connection details to connect with MySQL database.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=todo_app
DB_USERNAME=root
DB_PASSWORD=

Configure Blade Templates

Now will configure Blade templates. We will create resources/views/todos.blade.php file and copy paste below code to load application pages.

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="csrf-token" content="{{ csrf_token() }}">
        <title>Laravel Vue TodoList App</title>
		<link href="https://fonts.googleapis.com/css2?family=Baloo+Da+2:wght@500&display=swap" rel="stylesheet">
        <link rel="stylesheet" href="{{ asset('css/app.css') }}">
        <script src="https://use.fontawesome.com/186d66fd50.js"></script>
        <style>
            html, body {
                font-family: 'Baloo Da 2', cursive;
                background-color: #45eada;
            }
        </style>
    </head>
    <body>
        <main id="app">
          <todo-list-component></todo-list-component>
        </main>
        <script src="{{ asset('js/app.js') }}"></script>
    </body>
</html>

Add Vue Components

We will create a template file TodoList.vue in resources/js/components and add below code for listing Todo list, add, edit and delete items.

<template>
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-sm-6">
                <div id="todo-container" class="p-3">
                    <section id="todo-header" class="mt-3">
                        <h3 class="text-left">Laravel Vue TodoList App123</h3>
                    </section>
                    <section id="todo-errors">
                        <div v-if="createTodoForm.errors.length > 0" class="alert alert-warning alert-dismissible fade show" role="alert">
                            <span v-for="(error, index) in createTodoForm.errors" :key="index">{{ error }}</span>
                            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                                <span aria-hidden="true">×</span>
                            </button>
                        </div>
                        <div v-if="createTodoForm.isCreated" class="alert alert-success alert-dismissible fade show" role="alert">
                            <span>Todo item created successfully</span>
                            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                                <span aria-hidden="true">×</span>
                            </button>
                        </div>
                        <div v-if="editTodoForm.isUpdated" class="alert alert-success alert-dismissible fade show" role="alert">
                            <span>Todo item updated successfully</span>
                            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                                <span aria-hidden="true">×</span>
                            </button>
                        </div>
                    </section>
                    <section id="add-todo-form" class="my-3">
                        <form>
                            <div class="d-flex justify-content-between align-items-center">
                                <input
                                    v-model="createTodoForm.name"
                                    v-on:keyup.enter="addTodo"
                                    minlength="5" maxlength="50"
                                    placeholder="Enter todo name and press enter"
                                    type="text" class="form-control mr-3">
                                <button v-if="createTodoForm.isSubmitting" class="btn btn-primary" type="button" disabled>
                                    <span class="spinner-grow spinner-grow-sm" role="status" aria-hidden="true"></span>
                                    <span class="sr-only">Loading...</span>
                                </button>
                                <button v-else @click.prevent="addTodo" class="btn">Add</button>
                            </div>
                        </form>
                    </section>
                    <section id="todo-actions"></section>
                    <section id="todo-list">
                        <ul class="list-group">
                            <div v-if="todos.isLoading" class="text-center">
                                <div class="spinner-border" role="status">
                                    <span class="sr-only">Loading...</span>
                                </div>
                            </div>
                            <li
                                v-if="!todos.isLoading && todos.data.length > 0"
                                v-for="todo in todos.data" :key="todo.uuid"
                                class="list-group-item">
                                <div class="d-flex justify-content-between align-items-center">
                                    {{ todo.name }}
                                    <span class="d-flex justify-content-between align-items-center">
                                        <a class="text-success mr-2" href="#" @click.prevent="showEditTodoForm(todo)">
                                            <i class="fa fa-edit"> </i> 
                                        </a>
                                        <a class="text-danger" href="#" @click.prevent="destroy(todo)">
                                            <i class="fa fa-trash-o"></i> 
                                        </a>
                                    </span>
                                </div>
                                <div class="d-flex w-100 justify-content-between">
                                    <small class="text-muted">Created</small>
                                    <small class="text-muted">{{ todo.created_at }}</small>
                                </div>
                            </li>
                            <li v-if="!todos.isLoading && todos.data.length === 0" class="list-group-item list-group-item-action list-group-item-warning">
                                No todo items found.
                            </li>
                        </ul>
                    </section>
                </div>
            </div>
        </div>
        <!-- Modal -->
        <div class="modal fade" id="editTodoItemModal" tabindex="-1" role="dialog" aria-labelledby="editTodoItemModalLabel" aria-hidden="true">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title" id="editTodoItemModalLabel">Edit TodoItem</h5>
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                            <span aria-hidden="true">×</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <section id="edit-todo-errors" class="mb-3">
                            <div v-if="editTodoForm.errors.length > 0" class="alert alert-warning alert-dismissible fade show" role="alert">
                                <span v-for="(error, index) in editTodoForm.errors" :key="index">{{ error }}</span>
                                <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                                    <span aria-hidden="true">×</span>
                                </button>
                            </div>
                        </section>
                        <section id="edit-todo">
                            <form>
                                <div class="d-flex justify-content-between align-items-center">
                                    <input
                                        v-model="editTodoForm.name"
                                        v-on:keyup.enter="updateTodo"
                                        minlength="5" maxlength="50"
                                        placeholder="Enter todo name and press enter"
                                        type="text" class="form-control mr-3">
                                    <button v-if="editTodoForm.isSubmitting" class="btn btn-primary" type="button" disabled>
                                        <span class="spinner-grow spinner-grow-sm" role="status" aria-hidden="true"></span>
                                        <span class="sr-only">Loading...</span>
                                    </button>
                                    <button v-else @click.prevent="updateTodo" class="btn btn-success">Save</button>
                                </div>
                            </form>
                        </section>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

We will also add blow JavaScript code after template HTML to manage Todo items with Vue. We will implement functions loadTodos(), addTodo(), updateTodo() and destroy() to handle functionality.


<script>
    export default {
        name: 'TodoList',
        data() {
            return {
                todos: {
                    isLoading: false,
                    data: []
                },
                createTodoForm: {
                    isSubmitting: false,
                    isCreated: false,
                    name: undefined,
                    errors: []
                },
                editTodoForm: {
                    isSubmitting: false,
                    isUpdated: false,
                    uuid: undefined,
                    name: undefined,
                    errors: []
                },
                error: undefined
            }
        },
        mounted() {
            this.loadTodos();
        },
        methods: {
            showEditTodoForm(todo) {
                this.editTodoForm.name = todo.name;
                this.editTodoForm.uuid = todo.uuid;
                $('#editTodoItemModal').modal('show');
            },            
            loadTodos() {
                this.todos.isLoading = true;

                axios.get('/todos')
                .then((response) => {
                    this.todos.data = response.data;
                })
                .catch((error) => {                    
                    this.error = 'Unable to load todos. View log for more information';
                })
                .finally(() => {                    
                    this.todos.isLoading = false;
                })
            },            
            addTodo() {                
                this.createTodoForm.isSubmitting = true;               
                axios.post('/todos', this.createTodoForm)
                .then((response) => {
                    this.createTodoForm.errors = [];
                    this.createTodoForm.name = undefined;
                    this.createTodoForm.isCreated = true;
                    this.loadTodos();
                })
                .catch((error) => {                    
                    if (error.response && error.response.status === 422) {
                        this.createTodoForm.errors = _.flatten(_.toArray(error.response.data.errors));
                    } else {   
                        this.error = 'Unable to add todo. View log for more information';
                    }
                })
                .finally(() => {                    
                    this.createTodoForm.isSubmitting = false;
                })
            },            
            updateTodo() {                
                this.editTodoForm.isSubmitting = true;                
                axios.put('/todos/${this.editTodoForm.uuid}', this.editTodoForm)
                    .then((response) => {
                        $('#editTodoItemModal').modal('hide');
                        this.editTodoForm.name = undefined;
                        this.editTodoForm.uuid = undefined;
                        this.editTodoForm.isUpdated = true;
                        this.loadTodos();
                    })
                    .catch((error) => {                       
                        if (error.response && error.response.status === 422) {
                            this.editTodoForm.errors = _.flatten(_.toArray(error.response.data.errors));
                        } else {                            
                            this.error = 'Unable to update todo. View log for more information';
                        }
                    })
                .finally(() => {                    
                    this.editTodoForm.isSubmitting = false;
                })
            },            
            destroy(todoItem) {                
                axios.delete('/todos/${todoItem.uuid}')
                    .then((response) => {
                        this.loadTodos();
                    })
                    .catch((error) => {                        
                        this.error = 'Unable to delete todo. View log for more information';
                    })
            }
        }
    }
</script>

We will also created TodoItem.vue to manage items. We will add below code to this file.

<template<

</template<

<script<
    export default {
        name: "TodoItem"
    }
</script<

<style scoped<

</style<

Add Vue Components To App.js

We will add componenets TodoList.vue to resources/js/app.js file. We will replace code with below code in resources/js/app.js file.

require('./bootstrap');

window.Vue = require('vue');

Vue.component('todo-list-component', require('./components/TodoList.vue').default);

const app = new Vue({
    el: '#app',
});

Create TodoItem Modal

We will create app/TodoItem.php app file and add below code. Create class TodoItem and extends model.

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Ramsey\Uuid\Uuid;

class TodoItem extends Model
{
    
    Use SoftDeletes;

    protected $hidden = [
        'id', 'deleted_at'
    ];

    protected static function boot() {
        parent::boot();
        self::creating(function($todoInstance) {
            $todoInstance->uuid = Uuid::uuid4()->toString();
        });
    }

    public function getRouteKeyName(){
        return 'uuid';
    }

    public function getCreatedAtAttribute($value){
        return Carbon::parse($value)->toFormattedDateString();
    }
}

Implement TodoItem Controller

We will create TodoItemController.php and use model TodoItem.php. We will implement methods todos(), store(), update() and destroy() to handle functionality. We will add below code in this file.

<?php

namespace App\Http\Controllers;

use App\TodoItem;
use Illuminate\Http\Request;

class TodoItemController extends Controller {
    public function index(){
        return view('todos');
    }
    
    public function todos(Request $request)
    {
        return response()->json(TodoItem::all());
    }

    public function store(Request $request){
        
        $this->validate($request, [
            'name' => [ 'required',  'string', 'min:5', 'max:50', 'unique:todo_items,name' ]
        ]);

        $todoItem = new TodoItem();
        $todoItem->name = $request->name;
        $todoItem->save();

        return $todoItem;
    }

    public function update(Request $request, TodoItem $todoItem){
       
        $this->validate($request, [
            'name' => [ 'required', 'string', 'min:5', 'max:50', 'unique:todo_items,name,'.$todoItem->id ]
        ]);

        $todoItem->name = $request->name;
        $todoItem->save();

        return $todoItem;
    }

    public function destroy(Request $request, TodoItem $todoItem)
    {
        $todoItem->delete();
        return response()->json(null, 204);
    }
}

Setup Routes

Now finally we will setup Laravel project routes. For this we will update routes in routes/web.php file to load Todos, add, edit and delete item.


Route::get('/', 'TodoItemController@index')->name('showTodosPage');
Route::get('/todos', 'TodoItemController@todos')->name('loadTodos');
Route::post('/todos', 'TodoItemController@store')->name('storeTodos');
Route::put('/todos/{todoItem}', 'TodoItemController@update')->name('updateTodo');
Route::delete('/todos/{todoItem}', 'TodoItemController@destroy')->name('deleteTodo');

Run The Application

Now we will run our Laravel application.

We will run below command to install node.js dependencies.

npm install && npm run dev

We will run application using below command.

php artisan serve --port 8080

The application can be accessed on localhost using below.

http://127.0.0.1:8000/