What is a Single Page Application?
A single page application (SPA) is a website development approach that enables a user to visit different parts/pages of a website without loading new HTML pages from the server. A single page application gives the feel of a desktop or mobile app
In this tutorial, we will be building a To-do list app (Single Page Application) that will enable us to add, delete, update and view all pending tasks using Laravel for its backend and VueJS for its frontend
Laravel is a php framework developed by Taylor Otwell is a PHP based web-framework with a lot of out of the box functionalities developed to ease the process in building medium to large web applications.
VueJS is a javascript framework developed by Evan You is a progressive Javascript framework that allows you to create a beautiful user interface and experience easily.
Let’s get started 🙂
Laravel Installation
- Open your terminal and go to the directory where you want to store your project.
- Before installation ensure you meet the laravel installation requirements here: https://laravel.com/docs/6.x#server-requirements
- Install laravel by typing the following command in your terminal
composer create-project --prefer-dist laravel/laravel todo_list_app
- Once the installation is complete, enter into the project directory and start the app
cd todo_list_app php artisan serve
- Laravel will assign a web address to you on your terminal as seen below;
$ php artisan serve Laravel development server started: localhost:8000
copy the web address from your terminal to your preferred web browser to confirm your installation was successful by seeing the image below.
Laravel .env file
The .env file located at the root directory of the laravel project contains configuration variables for our application as seen below. Before we continue this tutorial ensure you put the right variables for the following keys DB_DATABASE, DB_USERNAME, DB_PASSWORD
APP_NAME=Laravel APP_ENV=local APP_KEY=base64:MGFGyZ4T8XFCY1fkxqRJw1f6WQiL+odkmu05B9kmcGM= APP_DEBUG=true APP_URL=http://localhost LOG_CHANNEL=stack DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=todo_list_app DB_USERNAME=root DB_PASSWORD= BROADCAST_DRIVER=log CACHE_DRIVER=file QUEUE_CONNECTION=sync SESSION_DRIVER=cookie SESSION_LIFETIME=120 REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 MAIL_DRIVER=smtp MAIL_HOST=smtp.mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET= PUSHER_APP_ID= PUSHER_APP_KEY= PUSHER_APP_SECRET= PUSHER_APP_CLUSTER=mt1 MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
Note: It is expected that your development server has been setup
Create Migrations for database
According to the laravel documentation: Migrations are like version control for your database, allowing your team to easily modify and share the application’s database schema. Migrations are typically paired with Laravel’s schema builder to easily build your application’s database schema. If you have ever had to tell a teammate to manually add a column to their local database schema, you’ve faced the problem that database migrations solve.
In our project, our database will have only one table and we will name it tasks. Delete all the sample files in the database > migrations directory, we will not be using them in this tutorial. Let’s create our migration file.
- Create migration for tasks table. Open your terminal and navigate to your project directory then run the command
php artisan make:migration create_tasks_table
- A migration file will be created in database > migrations directory, open the latest file created and copy the code below into it
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateTasksTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('tasks', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('title', 191); $table->text('description'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('tasks'); } }
3. Then run the command below to create the tasks table in our database
php artisan migrate
Create a Model for tasks Table
Models allow us to query for data in our tables, as well as insert new records into the table. Models are stored in the app directory
- Open your terminal and navigate into the project directory, then run the command below
php artisan make:model Task
Task.php file will be created in the app directory
Create a Task Controller
Controllers group related request handling/business logic into a single class. Controllers are stored in the app > Http > Controllers directory.
- Open your terminal and navigate into the project directory, then the run command below
php artisan make:controller TaskController --resource
- Then copy the code below into your TaskController.php file in app > Http > Controllers directory
<?php namespace App\Http\Controllers; use App\Task; use Illuminate\Http\Request; class TaskController extends Controller { //all tasks public function index() { $tasks = Task::all(); return response()->json($tasks); } //create new task public function create(Request $request) { $task = new Task; $task->title = $request->get('title'); $task->description = $request->get('description'); $task->save(); return response()->json('The task was successfully added'); } //edit Task public function edit($id) { $task = Task::find($id); return response()->json($task); } //update task public function update(Request $request, $id) { $task = Task::find($id); $task->title = $request->get('title'); $task->description = $request->get('description'); $task->save(); return response()->json('The task was updated successfully'); } //delete task public function delete($id) { $task = Task::find($id); $task->delete(); return response()->json('The task was deleted successfully'); } }
Define API routes
The API routes would be consumed by our frontend to send and receive data based on the business logic in our TaskController.php
Copy the code below to api.php in the routes directory
<?php /* |-------------------------------------------------------------------------- | API Routes |-------------------------------------------------------------------------- | | Here is where you can register API routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | is assigned the "api" middleware group. Enjoy building your API! | */ Route::get('tasks', 'TaskController@index'); Route::post('create', 'TaskController@create'); Route::get('edit/{id}', 'TaskController@edit'); Route::post('update/{id}', 'TaskController@update'); Route::delete('delete/{id}', 'TaskController@delete');
***We are done with the Laravel backend implementation, to test what we have done so far, you can use Postman to test our APIs. Ensure you have run the php artisan serve command in your terminal. For example to create a new task you will send a post request to http://localhost:8000/api/create.
**http://localhost:8000 could be different depending on your machine
VueJS Installation
Next, we implement the frontend integration for our APIs created using VueJS
- Open your terminal and navigate into the project directory, then install the frontend node modules dependencies
npm install
- Next, we will install Vue;
npm install --save vue
- We will then install Vue-router; Vue-router is the official router package for Vue. It allows us to navigate to different parts of our website without the web page reloading
npm install vue-router
- We will also install Axios; Axios is a Javascript library used to make HTTP requests. It allows us to send and receive data from our laravel API endpoints that we will create later
npm install axios
- Then run the command below to ensure everything is working fine.
npm run watch
Note: npm run watch ensures that your Vue (frontend) files re-compile whenever a change is made on any of the files.
Linking VueJS with Laravel
- Define laravel route in web.php under the routes directory.
<?php //web.php Route::get('{any}', function () { return view('app'); })->where('any', '.*');
Note: This ensures that Laravel is no longer in charge of routing to different parts of the website but VueJS
- Next, we will create an app.blade.php file in resources > views directory, which will contain the app component that Vue will render along with rendering appropriate content based on the web URL.
<!doctype html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="csrf-token" value="{{ csrf_token() }}" /> <title>To-Do List App Tutorial - Tolustar</title> <link href="https://fonts.googleapis.com/css?family=Nunito:200,300,400,500,600" rel="stylesheet" type="text/css"> <link href="{{ mix('css/app.css') }}" type="text/css" rel="stylesheet" /> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"> </head> <body> <div id="app"> </div> <script src="{{ mix('js/app.js') }}" type="text/javascript"></script> <!-- Bootstrap JS and dependencies --> <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script> </body> </html>
Note: We also installed Bootstrap framework via CDN by adding its respective CSS links and JS scripts in our app.blade.php file
- Create a components directory in resources > js directory
- Create an App.vue file in the components directory resources > js > components. This file serves as the outermost component of the application that will enable all other components to be displayed
<template> <div class="container"> <div class="text-center" style="margin: 20px 0px 20px 0px;"> <h3 class="text-secondary">Laravel & Vue CRUD Single Page Application (SPA) Tutorial</h3> </div> <nav class="navbar navbar-expand-lg navbar-light bg-light"> <div class="collapse navbar-collapse"> <div class="navbar-nav"> <router-link to="/" class="nav-item nav-link">Home</router-link> <router-link to="/create" class="nav-item nav-link">Create Task</router-link> </div> </div> </nav> <br/> <router-view></router-view> </div> </template> <script> export default {} </script>
Create Frontend Pages
- Create pages directory in resources > js > components
- In the pages directory resources > js > components > pages create the following files AllTasks.vue, NewTask.vue, EditTask.vue
- Open the AllTasks.vue file and copy the code below
<template> <div> <h3 class="text-center">All Tasks</h3><br/> <table class="table table-bordered"> <thead> <tr> <th>ID</th> <th>Title</th> <th>Description</th> <th>Created At</th> <th>Updated At</th> <th>Actions</th> </tr> </thead> <tbody> <tr v-for="task in tasks" :key="task.id"> <td>{{ task.id }}</td> <td>{{ task.title }}</td> <td>{{ task.description }}</td> <td>{{ task.created_at }}</td> <td>{{ task.updated_at }}</td> <td> <div class="btn-group" role="group"> <router-link :to="{name: 'edit', params: { id: task.id }}" class="btn btn-primary">Edit </router-link> <button class="btn btn-danger" @click="deleteTask(task.id)">Delete</button> </div> </td> </tr> </tbody> </table> </div> </template> <script> export default { data() { return { tasks: [] } }, created() { this.axios .get('http://localhost:8000/api/tasks') .then(response => { this.tasks = response.data; }); }, methods: { deleteTask(id) { this.axios .delete(`http://localhost:8000/api/delete/${id}`) .then(response => { let i = this.tasks.map(item => item.id).indexOf(id); // find index of your object this.tasks.splice(i, 1) }); } } } </script>
- Open the NewTask.vue file and copy the code below
<template> <div> <h3 class="text-center">New Task</h3> <div class="row"> <div class="col-md-12"> <form @submit.prevent="newTask"> <div class="form-group"> <label>Title</label> <input type="text" class="form-control" v-model="task.title"> </div> <div class="form-group"> <label>Description</label> <input type="text" class="form-control" v-model="task.description"> </div> <button type="submit" class="btn btn-primary">Add Task</button> </form> </div> </div> </div> </template> <script> export default { data() { return { task: {} } }, methods: { newTask() { this.axios .post('http://localhost:8000/api/create', this.task) .then(response => ( this.$router.push({name: 'home'}) )) .catch(error => console.log(error)) .finally(() => this.loading = false) } } } </script>
- Open the EditTask.vue file and copy the code below
<template> <div> <h3 class="text-center">Edit Task</h3> <div class="row"> <div class="col-md-12"> <form @submit.prevent="updateTask"> <div class="form-group"> <label>Title</label> <input type="text" class="form-control" v-model="task.title"> </div> <div class="form-group"> <label>Description</label> <input type="text" class="form-control" v-model="task.description"> </div> <button type="submit" class="btn btn-primary">Update Task</button> </form> </div> </div> </div> </template> <script> export default { data() { return { task: {} } }, created() { this.axios .get(`http://localhost:8000/api/edit/${this.$route.params.id}`) .then((response) => { this.task = response.data; }); }, methods: { updateTask() { this.axios .post(`http://localhost:8000/api/update/${this.$route.params.id}`, this.task) .then((response) => { this.$router.push({name: 'home'}); }); } } } </script>
**Note: In the three files AllTasks.vue, NewTask.vue, EditTask.vue we used Axios to consume the laravel APIs
Define Vue Routes
We define the Vue routes in a route.js file in resources > js directory. Defining the vue routes ensures that we can navigate to different parts of our web app without encountering an error.
import AllTasks from './components/pages/AllTasks.vue'; import NewTask from './components/pages/NewTask.vue'; import EditTask from './components/pages/EditTask.vue'; export const routes = [ { name: 'home', path: '/', component: AllTasks }, { name: 'create', path: '/create', component: NewTask }, { name: 'edit', path: '/edit/:id', component: EditTask } ];
Importing All to App.js
We bring everything together by importing all the necessary dependencies that we have used so far such as Axios, Vue-router, routes, and also ensure our app is mounted to the DOM in the app.js file found in resources > js directory.
import Vue from 'vue' import App from './components/App.vue'; import VueRouter from 'vue-router'; import axios from 'axios'; import {routes} from './routes'; Vue.use(VueRouter); Vue.prototype.axios = axios; const router = new VueRouter({ mode: 'history', routes: routes }); const app = new Vue({ el: '#app', router: router, render: h => h(App), });
Final Output
We have completed the tutorial for today, let us run our project and see how it looks like
Conclusion
In today’s tutorial, we have learnt how to build a single page app with Laravel and VueJS. In future tutorials, I will explain how to secure certain API endpoints from unauthorized users and authenticate users from accessing restricted data based on user roles.
Do let me know if you have any question on this tutorial with the comment box below and don’t forget to share and like this tutorial. Cheers