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

Introduction

Introduction

In this tutorial, we will build a to-do app with Vue.js and Laravel using Pusher to add the realtime functionality. Vue.js is such a great JS framework as it speeds up frontend development and helps achieve more with less code. Laravel on its side is one the most popular and powerful backend frameworks; allied with Vue.js we can build awesome web apps. Pusher you may know is a collection of hosted APIs to build realtime apps with less code. Now let’s get started!

Demo

This is the result of our final working app:

todo-vue-demo

Prerequisites

In order to follow this tutorial a basic or good understanding of Vue.js and Laravel is required, as we’ll be using these technologies throughout this tutorial. Also ensure you have npm or Yarn on your machine.

We’ll be using these tools to build our application:

Pusher setup

Head over to the Pusher website and sign up for a free account. Select Create new app on the sidebar, and hit Create my app to create a new app after filling the form.

Once your app is created, retrieve your credentials from the API Keys tab, and make note of them as we’ll use them later in the tutorial.

Initialize the project and install dependencies

To get started we’ll install a new Laravel application using the Laravel CLI. We’ll run the following command:

laravel new realtime_todo

Once the installation is finished run the following command to move to your app directory:

cd realtime_todo

Now we’ll install our node dependencies, first paste this in your package.json file:

1{
2      "private": true,
3      "scripts": {
4        "dev": "npm run development",
5        "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
6        "watch": "npm run development -- --watch",
7        "watch-poll": "npm run watch -- --watch-poll",
8        "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
9        "prod": "npm run production",
10        "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
11      },
12      "devDependencies": {
13        "axios": "^0.18",
14        "bootstrap": "^4.0.0",
15        "cross-env": "^5.1",
16        "jquery": "^3.2",
17        "laravel-mix": "^2.0",
18        "lodash": "^4.17.5",
19        "popper.js": "^1.12",
20        "vue": "^2.5.7",
21        "vuex": "^3.0.1",
22        "laravel-echo": "^1.4.0",
23        "pusher-js": "^4.2.2"
24      }
25    }

Then run npm install or yarn to install the dependencies. It’s up to you.

After this step, add the following to your .env file in the root of your project directory. Ensure to replace the placeholders with your keys from Pusher.

1PUSHER_APP_ID=YOUR_PUSHER_APP_ID
2    PUSHER_APP_KEY=YOUR_PUSHER_APP_KEY
3    PUSHER_APP_SECRET=YOUR_PUSHER_APP_SECRET
4    PUSHER_APP_CLUSTER=YOUR_PUSHER_APP_CLUSTER

Database setup

In this tutorial, we’ll use SQLite as our database. Create a database.sqlite file in the database directory, and amend the .env file like this:

1DB_CONNECTION=sqlite
2    DB_DATABASE=/absolute/path/to/database.sqlite

Refer to this section on Laravel website for more relevant information.

Building models and seeding our database

Now, let’s build our database structure. We’ll use the Laravel CLI again for that. Run this command:

php artisan make:model Task -mc

The above command will generate the Task model as well as its migration and its controller TaskController.php for us.

Open your Task.php file and paste this:

1//realtime_todo/app/Task.php
2    
3    <?php
4    namespace App;
5    use Illuminate\Database\Eloquent\Model;
6    class Task extends Model
7    {
8        //
9        protected $fillable = ['title','completed'];
10    }

Next copy and paste this piece of code in your task migration file:

1//realtime_todo/database/migrations/*_create_tasks_table.php
2    
3    <?php
4    use Illuminate\Support\Facades\Schema;
5    use Illuminate\Database\Schema\Blueprint;
6    use Illuminate\Database\Migrations\Migration;
7    class CreateTasksTable extends Migration
8    {
9        /**
10        * Run the migrations.
11        *
12        * @return void
13        */
14    
15        public function up() {
16            Schema::create('tasks', function (Blueprint $table) {
17                $table->increments('id');
18                $table->string('title');
19                $table->boolean('completed')->default(false);
20                $table->timestamps();
21            }
22            );
23        }
24    
25        /**
26        * Reverse the migrations.
27        *
28        * @return void
29        */
30          public function down(){
31            Schema::dropIfExists('tasks');
32        }
33    }

Then run php artisan migrate to run the migration.

Define routes and create the TaskController

In this section, we’ll define our app endpoints and define the logic behind our TaskController.php

This is a simple CRUD(create, read, update, delete) over our Task model. So we defined our routes with corresponding functions to handle our browser requests. Paste the following into api.php:

1//realtime_todo/routes/api.php
2    <?php
3    use Illuminate\Http\Request;
4    
5    /*
6    --------------------------------------------------------------------------
7    | API Routes
8    |--------------------------------------------------------------------------
9    |
10    | Here is where you can register API routes for your application. These
11    | routes are loaded by the RouteServiceProvider within a group which
12    | is assigned the "api" middleware group. Enjoy building your API!
13    |
14    */
15    
16    Route::get('todos','TaskController@fetchAll');
17    Route::post('todos','TaskController@store');
18    Route::delete('todos/{id}','TaskController@delete');

Now let’s define our controller logic. Our controller functions will be responsible for actions to handle when some requests reach our API endpoints.

Open your TaskController file and paste the following code:

1//realtime_todo/app/Http/Controllers/TaskController.php
2    
3    <?php
4    
5    namespace App\Http\Controllers;
6    use Illuminate\Http\Request;
7    use App\Events\TaskCreated;
8    use App\Events\TaskRemoved;
9    use App\Task;
10    class TaskController extends Controller
11    {
12        //
13        public function fetchAll(){
14            $tasks = Task::all();
15            //return response()->json($tasks);
16            return $tasks;
17        }
18        
19        public function store(Request $request){
20            $task = Task::create($request->all());
21            broadcast(new TaskCreated($task));
22            return response()->json("added");
23        }
24        
25        public function delete($id){
26            $task = Task::find($id);
27            broadcast(new TaskRemoved($task));
28            Task::destroy($id);
29            return response()->json("deleted");
30        }
31    }

In the above code we have three functions fetchAll, store , and delete:

  • fetchAll: queries our database to return all our tasks/to-dos
  • store: creates a new to-do with request params(title and task status)
  • delete: finds a task and deletes from the database.

Emit events

Well you may have noticed these lines: broadcast(new Taskcreated($task)) and broadcast(new TaskRemoved($task)) respectively in store and delete functions. What is their purpose? Through these lines we emit events with a task model instance.

You can get more relevant information about Laravel broadcasting here. In the next part of the tutorial, we’ll see how to create these events..

Create our events

In this part we’ll create events we talked about above: TaskCreated and TaskRemoved events.

TaskCreated event

Our TaskCreated event will be emitted whenever a new to-do or task is created. Enough talk, let’s focus on the code. Let’s create our TaskCreated by running the following command in your terminal: php artisan make:event TaskCreated.

Now open your TaskCreated file and paste the following:

1//realtime_todo/app/Events/TaskCreated.php
2    <?php
3    
4    namespace App\Events;
5    use Illuminate\Broadcasting\Channel;
6    use Illuminate\Queue\SerializesModels;
7    use Illuminate\Broadcasting\PrivateChannel;
8    use Illuminate\Broadcasting\PresenceChannel;
9    use Illuminate\Foundation\Events\Dispatchable;
10    use Illuminate\Broadcasting\InteractsWithSockets;
11    use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
12    use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
13    
14    
15    class TaskCreated implements ShouldBroadcastNow
16    {
17        use Dispatchable, InteractsWithSockets, SerializesModels;
18    
19        /**
20        * Create a new event instance.
21        *
22        * @param $task
23        * @return void
24        */
25        
26        public $task;
27        public function __construct($task)
28        {
29            //
30            $this->task = $task;
31        }
32    
33        /**
34        * Get the channels the event should broadcast on.
35        *
36        * @return \Illuminate\Broadcasting\Channel|array
37        */
38        
39        public function broadcastOn()
40        {
41            return new Channel('newTask');
42        }
43        
44        public function broadcastAs(){
45            return 'task-created';
46        }
47        
48    }

Our class constructor initializes a task that is created. We have two additional functions that may seem strange to you:

  • broadcastAs: customizes the broadcast name because by default Laravel uses the event’s class name.
  • broadcastOn: defines the channel task-created (which we’ll set up further on the tutorial) on which our event should be broadcast.

TaskRemoved event

This event is broadcast when a task is deleted and we want to notify other users of that.

As you may guess, you can run: php artisan make:event TaskRemoved to create the event. Now head up to your TaskRemoved file:

1//realtime_todo/app/Events/TaskRemoved.php
2    
3    <?php
4    namespace App\Events;
5    use Illuminate\Broadcasting\Channel;
6    use Illuminate\Queue\SerializesModels;
7    use Illuminate\Broadcasting\PrivateChannel;
8    use Illuminate\Broadcasting\PresenceChannel;
9    use Illuminate\Foundation\Events\Dispatchable;
10    use Illuminate\Broadcasting\InteractsWithSockets;
11    use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
12    use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
13    class TaskRemoved implements ShouldBroadcastNow
14    {
15        use Dispatchable, InteractsWithSockets, SerializesModels;
16    
17        /**
18        * Create a new event instance.
19        *
20        *  @param $task
21        * @return void
22        *
23        */
24            public $task;
25            public function __construct($task)
26            {
27            //
28            $this->task = $task;
29        }
30    
31        /**
32        * Get the channels the event should broadcast on.
33        *
34        * @return \Illuminate\Broadcasting\Channel|array
35        */
36        
37        public function broadcastOn()
38        {
39            return new Channel('taskRemoved');
40        }
41        
42        public function broadcastAs(){
43            return 'task-removed';
44        }
45        
46    }

This class structure is pretty similar to the previous one, so we won't spend further time explaining its functions.

Don’t forget to implement ShouldBroadcastNow to enable Pusher broadcasting events as soon as they occur.

Broadcast configuration

According to Laravel documentation about event broadcasting, before broadcasting any events, you will first need to register the App\Providers\BroadcastServiceProvider. In fresh Laravel applications, you only need to uncomment this provider in the providers array of your ../config/app.php configuration file. This provider will allow you to register the broadcast authorization routes and callbacks.

You may also set the encrypted value to false in order to avoid a 500 error while broadcasting events with Pusher.

If this is done, you have to tell Laravel to use Pusher to broadcast events. Open your .env file and ensure you have this line: BROADCAST_DRIVER=pusher

As we are broadcasting our events over Pusher, we should install the Pusher PHP SDK using the Composer package manager:

    composer require pusher/pusher-php-server "~3.0"

Setting up the broadcast channel

Laravel broadcasts events on well defined channels. As said above our events should be broadcast on particular channels. It’s time to set them up. Paste the following code in your channels.php file:

1//realtime_todo/routes/channels.php
2    <?php
3    
4    /*
5    ------------------------------------------------------------------------
6    | Broadcast Channels
7    |--------------------------------------------------------------------------
8    |
9    | Here you may register all of the event broadcasting channels that your
10    | application supports. The given channel authorization callbacks are
11    | used to check if an authenticated user can listen to the channel.
12    |
13    */
14    Broadcast::channel('newTask', function(){
15        return true;
16    });
17    Broadcast::channel('taskRemoved', function(){
18        return true;
19    });

As we aren’t using Laravel auth, we return true in the function callback so that everybody can use this channel to broadcast events.

Set up Laravel Echo

We’ll use Laravel Echo to consume our events on the client-side.

Open your resources/js/bootstrap.js file and uncomment this section of the code:

1import Echo from 'laravel-echo'
2    
3    window.Pusher = require('pusher-js');
4    
5    window.Echo = new Echo({
6        broadcaster: 'pusher',
7        key: process.env.MIX_PUSHER_APP_KEY,
8        cluster: process.env.MIX_PUSHER_APP_CLUSTER,
9        encrypted: false
10    });

While uncommenting, you may also set the encrypted property to false to avoid a 500 error while trying to broadcast events with Pusher channels.

The above code sets up Laravel Echo with Pusher. This will make our app aware of events broadcast, and Laravel Echo will consume our events

Our app is ready to broadcast and consume events in realtime using Pusher channels. Let’s focus now on the frontend of your app.

Set up Vuex store

We’ll be using the Vuex library to centralize our data and control the way it is mutated throughout our application.

Create our state

Vuex state is a single object that contains all our application data. So let’s create ../resources/js/store/state.js and paste this code inside:

1let state = {
2        todos: [],
3        toRemove: null,
4        newTodo: {
5            title: '',
6            completed: false
7        }
8    }
9    export default state

Our state objects holds :

  • todos: holds our to-dos got from the backend
  • toRemove: holds temporarily the to-do we intend to remove
  • newTodo: holds details about a new to-do we are about to add

Create our getters

With help of getters we can compute derived state based on our data store state. Create ../resources/js/store/getters.js and paste this code inside

1let getters = {
2        newTodo: state => {
3            return state.newTodo
4        },
5        todos: state => {
6            return state.todos
7        },
8        toRemove: state => {
9            return state.toRemove
10        }
11    }
12    export default getters

Create our mutations

Mutations allow us to perform some changes on our data. Create ../resources/js/store/mutations.js and paste this piece of code inside:

1let mutations = {
2        ADD_TODO(state, todo) {
3            state.todos.unshift(todo)
4        },
5        CACHE_REMOVED(state, todo) {
6          state.toRemove = todo;
7        },
8        GET_TODOS(state, todos) {
9            state.todos = todos
10        },
11        DELETE_TODO(state, todo) {
12            state.todos.splice(state.todos.indexOf(todo), 1)
13            state.toRemove = null;
14        }
15    }
16    export default mutations

Here we have three mutations:

  • ADD_TODO: adds a new to-do to the top our to-dos list
  • CACHE_REMOVED: keeps track temporarily of the to-do to remove
  • GET_TODOS: sets our to-dos list given some data
  • DELETE_TODO: responsible for deleting a to-do from our to-dos list

Create our actions

Vuex actions allow us to perform asynchronous operations over our data. Create the file ../resources/js/store/actions.js and paste the following code:

1let actions = {
2      ADD_TODO({commit}, todo) {
3            axios.post('/api/todos', todo).then(res => {
4                if (res.data === "added")
5                    console.log('ok')
6            }).catch(err => {
7                console.log(err)
8            })
9        },
10        DELETE_TODO({commit}, todo) {
11            axios.delete(`/api/todos/${todo.id}`)
12                .then(res => {
13                    if (res.data === 'deleted')
14                        console.log('deleted')
15                }).catch(err => {
16                    console.log(err)
17                })
18        },
19        GET_TODOS({commit}) {
20            axios.get('/api/todos')
21                .then(res => {
22                    {  console.log(res.data)
23                        commit('GET_TODOS', res.data)
24                    }
25                }).catch(err => {
26                    console.log(err)
27                })
28        }
29    }
30    export default actions

We have defined two actions and each of them responsible of a single operation. They perform asynchronous calls to our API routes.

  • ADD_TODO makes a POST request to our api/todos endpoint to add a new task. This action is dispatched whenever the user is submitting a task to add.

  • GET_TODOS sends a GET request to our api/todos endpoint to get our database to-dos/tasks and commits the response with GET_TODOS mutation.

  • DELETE_TODO performs a DELETE a request to our api/todos/{id} endpoint in order to remove a to-do from our to-dos list.

Set up our store with Vue

Create the file ../resources/js/store/index.js and paste this code inside:

1import Vue from 'vue'
2    import Vuex from 'vuex'
3    import actions from './actions'
4    import mutations from './mutations'
5    import getters from './getters'
6    import state from "./state";
7    
8    Vue.use(Vuex);
9    
10    export default new Vuex.Store({
11        state,
12        mutations,
13        getters,
14        actions
15    })

Then, we export our store and add it to the Vue instance. Replace the existing code in your ../resouces/js/app.js file with the following:

1require('./bootstrap');
2    window.Vue = require('vue');
3    
4    Vue.component('todo', require('./components/Todo'))
5    Vue.component('new-todo', require('./components/NewTodo.vue'))
6    Vue.component('todo-list', require('./components/TodoList'))
7    Vue.component('todo-app', require('./components/TodoApp'))
8    import store from '../js/store'
9    
10    const app = new Vue({
11        el: '#app',
12        store
13    });

The previous code also globally registers four Vue components, Todo.vue ,NewTodo.vue,TodoList.vue and TodoApp.vue that we’ll build in the next part of this tutorial.

Building Vue components

We’ll build four Vue components for our app as we said above, so let’s do it.

Create the Todo.vue component

The Todo.vue component is responsible for encapsulating details about a single Task instance from the database and rendering it in a proper and styled way. Create a Todo.vue file in your ../resources/js/components directory and paste the following inside:

1//../resources/js/components/Todo.vue
2    <template>
3     <li class="todo" :class="{ completed: todo.completed }">
4         <div class="view">
5             <input type="checkbox" v-model="todo.completed" class="toggle">
6             <label>{{todo.title}}</label>
7             <button @click="removeTodo(todo)" class="destroy"></button>
8         </div>
9     </li>
10    </template>
11    <script>
12    export default {
13      name: "Todo",
14      props: ["todo"],
15     
16      methods: {
17        removeTodo(todo) {
18          this.$store.commit("CACHE_REMOVED", todo)
19          this.$store.dispatch("DELETE_TODO", todo);
20        }
21      }
22    };
23    </script>

Our Todo.vue component takes a todo property whose details we render in the component body using the HTML <li></li> tag. The component has the removeTodo function that takes the to-do we intend to remove as argument, caches it temporarily (via the CACHE_REMOVED mutation) and dispatches the DELETE_TODO action to remove it.

Create the NewTodo.vue component

We’ll use this component to add a new task to our to-dos list. This component should be very simple to code. I promise you 😉. First create the NewTodo.vue file inside the same directory as above and paste this inside:

1//../resources/js/components/NewTodo.vue
2    <template>
3         <input type="text" v-model="newTodo.title"
4         @keyup.enter="addTodo" autofocus="autofocus"
5          placeholder="What are you trying to get done?" class="new-todo">
6    </template>
7    <script>
8    import { mapGetters } from "vuex";
9    export default {
10      name: "NewTodo",
11      methods: {
12        addTodo() {
13          this.$store.dispatch("ADD_TODO", this.newTodo);
14        }
15      },
16      computed: {
17        ...mapGetters(["newTodo"])
18      }
19    };
20    </script>

This component is composed of a simple input field to enter our to-do title. We append a @keyup.enter event so we can execute the addTodo function whenever the user hits the Enter key of his keyboard. Nice play isn’t it 😎 ?! We get the newTodo state object from our getters using Vue.js mapGetters helper and bind it to our input. As I said above the newTodo object should contain information of a new todo we want to add to our to-dos list. The addTodo function dispatches the ADD_TODO action having our newTodo as a parameter.

Create the TodoList.vue component

This component will render to-dos items from database. It’s that simple. So create your TodoList.vue component and paste this code inside:

1//../resources/js/components/TodoList.vue
2    <template>
3       <ul class="todo-list">
4         <todo v-for="todo in todos" :todo="todo" :key="todo.id" />
5       </ul>
6    </template>
7    
8    <script>
9    import { mapGetters } from "vuex";
10    import todo from "../components/Todo";
11    export default {
12      components: {
13        todo
14      },
15      name: "TodoList",
16      mounted() {
17        this.$store.dispatch("GET_TODOS");
18      },
19      computed: {
20        ...mapGetters(["todos"]),
21      }
22    };
23    </script>

In the mounted hook function we dispatch the GET_TODOS action to get our to-dos list item, and we use Vuex helper function …mapGetters() to access our todos state. We loop over our to-dos list and render a todo component (imported from Todo.vue component) which takes the current loop item as a property.

Create the TodoApp.vue component

In this component we simply merge the first three components we created and listen to Pusher realtime events. Create your TodoApp.vue component and paste the following inside:

1//../resources/js/components/TodoApp.vue
2    
3     <template>
4         <section class="todoapp">
5                <header class="header">
6                  <h1>todos</h1>
7                </header>
8                <new-todo></new-todo>
9                <todo-list></todo-list>
10         </section>
11     </template>
12     <script>
13    import newTodo from "../components/NewTodo.vue";
14    import todoList from "../components/TodoList.vue";
15    import { mapGetters } from "vuex";
16    
17    export default {
18      components: {
19       newTodo,
20       todoList
21      },
22      name: "TodoApp",
23      mounted() {
24        window.Echo.channel("newTask").listen(".task-created", e => {
25          this.$store.commit("ADD_TODO", e.task);
26          this.newTodo.title = "";
27        });
28        window.Echo.channel("taskRemoved").listen(".task-removed", e => {
29            this.$store.commit("DELETE_TODO", this.toRemove);
30        });
31      },
32      computed: {
33        ...mapGetters(["newTodo", "toRemove"])
34      }
35    };
36    </script>

In the mounted function of our component, we are subscribing to two channels:

  • newTask channel: we listen the task-created event triggered when a new to-do is added to the list. Then we commit the ADD_TODO mutation with the task sent carried by the event, in order to add it to our to-dos list. Finally we reset our newTodo we import from our Vuex store.

  • taskRemoved channel, this channel enables to listen to the task-removed event triggered when a task/to-do is removed from our list. When the event is emit, we assign the task deleted to our toRemove object we set up in our Vuex store, and we commit the DELETE_TODO mutation to finally remove it from to-dos list.

Finalize the app

Now, let’s replace of our welcome.blade.php with the following containing our TodoApp component:

1//realtime_todo/resources/views/welcome.blade.php
2    
3    <!doctype html>
4    <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
5        <head>
6            <meta charset="utf-8">
7            <meta name="viewport" content="width=device-width, initial-scale=1">
8            <meta name="csrf-token" content="{{ csrf_token() }}" />
9            <title>Realtime to-do app</title>
10            <!-- Fonts -->
11            <link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet" type="text/css">
12            {{--  Style  --}}
13            <link href="{{ mix('css/app.css') }}" rel="stylesheet" type="text/css">
14            <!-- Styles -->
15            <style>
16                html, body {
17                    background-color: #fff;
18                    color: #636b6f;
19                    font-family: 'Nunito', sans-serif;
20                    font-weight: 200;
21                    height: 100vh;
22                    margin: 0;
23                }
24                .full-height {
25                    height: 100vh;
26                }
27                .flex-center {
28                    align-items: center;
29                    display: flex;
30                    justify-content: center;
31                }
32                .position-ref {
33                    position: relative;
34                }
35                .top-right {
36                    position: absolute;
37                    right: 10px;
38                    top: 18px;
39                }
40                .content {
41                    text-align: center;
42                }
43                .title {
44                    font-size: 84px;
45                }
46                .links > a {
47                    color: #636b6f;
48                    padding: 0 25px;
49                    font-size: 12px;
50                    font-weight: 600;
51                    letter-spacing: .1rem;
52                    text-decoration: none;
53                    text-transform: uppercase;
54                }
55                .m-b-md {
56                    margin-bottom: 30px;
57                }
58            </style>
59        </head>
60        <body>
61              <div id="app">
62                        <todo-app></todo-app>
63              </div>
64    
65              <script src="{{mix('js/app.js')}}"></script>
66        </body>
67    </html>

To style our app, get this file and replace the content inside your ../resources/sass/app.scss file with it.

Now open your terminal and run npm run dev to build your app in a proper way. This can take a few seconds. After this step run php artisan serve and open your browser at localhost:8000 to see your app working fine. You can try to add a new to-do to your list and see things working in realtime if you carefully followed steps above. You are now a boss 😎

todo-vue-demo

Note: If you encounter a 500 error when trying to add or delete to-dos, it’s sure that you have to disable Pusher encryption as I suggested you. Open these files ../config/broadcasting.php and ../resources/js/bootstrap.js and make sure you disable Pusher encryption encrypted: false in both of them.

Conclusion

In this tutorial we’ve created a realtime to-do app using Laravel, Vue.js, and Pusher to provide realtime functionality. You can think up new ideas to extend the application. It’ll be fun to see what you come up with. The source code for this tutorial is available on GitHub here.