Build a CMS with Laravel and Vue - Part 4: Building the dashboard

Introduction

In the last article of this series, we built the API interface and used Laravel API resources to return neatly formatted JSON responses. We tested that the API works as we defined it to using Postman.

In this part of the series, we will start building the admin frontend of the CMS. This is the first part of the series where we will integrate Vue and explore Vue’s magical abilities.

When we are done with this part, our application will have some added functionalities as seen below:

laravel-vue-cms-demo-part-4

The source code for this project is available here on GitHub.

Prerequisites

To follow along with this series, a few things are required:

  • Basic knowledge of PHP.
  • Basic knowledge of the Laravel framework.
  • Basic knowledge of JavaScript (ES6 syntax).
  • Basic knowledge of Vue.

Building the frontend

Laravel ships with Vue out of the box so we do not need to use the Vue-CLI or reference Vue from a CDN. This makes it possible for us to have all of our application, the frontend, and backend, in a single codebase.

Every newly created instance of a Laravel installation has some Vue files included by default, we can see these files when we navigate into the resources/assets/js/components folder.

Setting up Vue and VueRouter

Before we can start using Vue in our application, we need to first install some dependencies using NPM. To install the dependencies that come by default with Laravel, run the command below:

    $ npm install

We will be managing all of the routes for the admin dashboard using vue-router so let’s pull it in:

    $ npm install --save vue-router

When the installation is complete, the next thing we want to do is open the resources/assets/js/app.js file and replace its contents with the code below:

1// File: ./resources/assets/js/app.js
2    require('./bootstrap');
3    
4    import Vue from 'vue'
5    import VueRouter from 'vue-router'
6    import Homepage from './components/Homepage'
7    import Read from './components/Read'
8    
9    Vue.use(VueRouter)
10    
11    const router = new VueRouter({
12        mode: 'history',
13        routes: [
14            {
15                path: '/admin/dashboard',
16                name: 'read',
17                component: Read,
18                props: true
19            },
20        ],
21    });
22    
23    const app = new Vue({
24        el: '#app',
25        router,
26        components: { Homepage },
27    });

In the snippet above, we imported the VueRouter and added it to the Vue application. We also imported a Homepage and a Read component. These are the components where we will write our markup so let’s create both files.

Open the resources/assets/js/components folder and create four files:

  • Homepage.vue - this will be the parent component for the admin dashboard frontend.
  • Read.vue - this will be component that displays all the available posts on the admin dashboard.
  • Create.vue - this will be the component where an admin user can create a new post.
  • Update.vue - this will be the component that displays the view where an admin user can update an existing post.

Note that we didn’t create a component file for the delete operation, this is because it is going to be possible to delete a post from the Read component. There is no need for a view.

In the resources/assets/js/app.js file, we defined a routes array and in it, we registered a read route. During render time, this route’s path will be mapped to the Read component.

In the previous article, we specified that admin users should be shown an admin.dashboard view in the index method, however, we didn’t create this view. Let’s create the view. Open the resources/views folder and create a new folder called admin. Within the new resources/views/admin folder, create a new file and called dashboard.blade.php. This is going to be the entry point to the admin dashboard, further from this route, we will let the VueRouter handle everything else.

Open the resources/views/admin/dashboard.blade.php file and paste in the following code:

1<!-- File: ./resources/views/admin/dashboard.blade.php -->
2    <!DOCTYPE html>
3    <html lang="en">
4    <head>
5        <meta charset="UTF-8">
6        <meta name="viewport" content="width=device-width, initial-scale=1.0">
7        <meta http-equiv="X-UA-Compatible" content="ie=edge">
8        <title> Welcome to the Admin dashboard </title>
9        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
10        <style>
11            html, body {
12            background-color: #202B33;
13            color: #738491;
14            font-family: "Open Sans";
15            font-size: 16px;
16            font-smoothing: antialiased;
17            overflow: hidden;
18            }
19        </style>
20    </head>
21    <body>
22    
23      <script src="{{ asset('js/app.js') }}"></script>
24    </body>
25    </html>

Our goal here is to integrate Vue into the application, so we included the resources/assets/js/app.js file with this line of code:

    <script src="{{ asset('js/app.js') }}"></script>

For our app to work, we need a root element to bind our Vue instance unto. Before the <script> tag, add this snippet of code:

1<div id="app">
2      <Homepage 
3        :user-name='@json(auth()->user()->name)' 
4        :user-id='@json(auth()->user()->id)'
5      ></Homepage>
6    </div>

We earlier defined the Homepage component as the wrapping component, that’s why we pulled it in here as the root component. For some of the frontend components to work correctly, we require some details of the logged in admin user to perform CRUD operations. This is why we passed down the userName and userId props to the Homepage component.

We need to prevent the CSRF error from occurring in our Vue frontend, so include this snippet of code just before the <title> tag:

1<meta name="csrf-token" content="{{ csrf_token() }}">
2    <script> window.Laravel = { csrfToken: 'csrf_token() ' } </script>

This snippet will ensure that the correct token is always included in our frontend, Laravel provides the CSRF protection for us out of the box.

At this point, this should be the contents of your resources/views/admin/dashboard.blade.php file:

1<!DOCTYPE html>
2    <html lang="en">
3    <head>
4        <meta charset="UTF-8">
5        <meta name="viewport" content="width=device-width, initial-scale=1.0">
6        <meta http-equiv="X-UA-Compatible" content="ie=edge">
7        <meta name="csrf-token" content="{{ csrf_token() }}">
8        <script> window.Laravel = { csrfToken: 'csrf_token() ' } </script>
9        <title> Welcome to the Admin dashboard </title>
10        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
11        <style>
12          html, body {
13            background-color: #202B33;
14            color: #738491;
15            font-family: "Open Sans";
16            font-size: 16px;
17            font-smoothing: antialiased;
18            overflow: hidden;
19          }
20        </style>
21    </head>
22    <body>
23    <div id="app">
24      <Homepage 
25        :user-name='@json(auth()->user()->name)' 
26        :user-id='@json(auth()->user()->id)'>
27      </Homepage>
28    </div>
29    <script src="{{ asset('js/app.js') }}"></script>
30    </body>
31    </html>

Setting up the Homepage view

Open the Homepage.vue file that we created some time ago and include this markup template:

1<!-- File: ./resources/app/js/components/Homepage.vue -->
2    <template>
3      <div>
4        <nav>
5          <section>
6            <a style="color: white" href="/admin/dashboard">Laravel-CMS</a> &nbsp; ||  &nbsp;
7            <a style="color: white" href="/">HOME</a>
8            <hr>
9            <ul>
10               <li>
11                 <router-link :to="{ name: 'create', params: { userId } }">
12                   NEW POST
13                 </router-link>
14               </li>
15            </ul>
16          </section>
17        </nav>
18        <article>
19          <header>
20            <header class="d-inline">Welcome, {{ userName }}</header>
21            <p @click="logout" class="float-right mr-3" style="cursor: pointer">Logout</p>
22          </header>
23          <div> 
24            <router-view></router-view> 
25          </div>
26        </article>
27      </div>
28    </template>

We added a router-link in this template, which routes to the Create component.

We are passing the userId data to the create component because a userId is required during Post creation.

Let’s include some styles so that the page looks good. Below the closing template tag, paste the following code:

1<style scoped>
2      @import url(https://fonts.googleapis.com/css?family=Dosis:300|Lato:300,400,600,700|Roboto+Condensed:300,700|Open+Sans+Condensed:300,600|Open+Sans:400,300,600,700|Maven+Pro:400,700);
3      @import url("https://netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.css");
4      * {
5        -moz-box-sizing: border-box;
6        -webkit-box-sizing: border-box;
7        box-sizing: border-box;
8      }
9      header {
10        color: #d3d3d3;
11      }
12      nav {
13        position: absolute;
14        top: 0;
15        bottom: 0;
16        right: 82%;
17        left: 0;
18        padding: 22px;
19        border-right: 2px solid #161e23;
20      }
21      nav > header {
22        font-weight: 700;
23        font-size: 0.8rem;
24        text-transform: uppercase;
25      }
26      nav section {
27        font-weight: 600;
28      }
29      nav section header {
30        padding-top: 30px;
31      }
32      nav section ul {
33        list-style: none;
34        padding: 0px;
35      }
36      nav section ul a {
37        color: white;
38        text-decoration: none;
39        font-weight: bold;
40      }
41      article {
42        position: absolute;
43        top: 0;
44        bottom: 0;
45        right: 0;
46        left: 18%;
47        overflow: auto;
48        border-left: 2px solid #2a3843;
49        padding: 20px;
50      }
51      article > header {
52        height: 60px;
53        border-bottom: 1px solid #2a3843;
54      }
55    </style>

We are using the scoped attribute on the <style> tag because we want the CSS to only be applied on the Homepage component.

Next, let's add the <script> section that will use the props we passed down from the parent component. We will also define the method that controls the log out feature here. Below the closing style tag, paste the following code:

1<script>
2    export default {
3      props: {
4        userId: {
5          type: Number,
6          required: true
7        },
8        userName: {
9          type: String,
10          required: true
11        }
12      },
13      data() {
14        return {};
15      },
16      methods: {
17        logout() {
18          axios.post("/logout").then(() => {
19            window.location = "/";
20          });
21        }
22      }
23    };
24    </script>

Setting up the Read view

In the resources/assets/js/app.js file, we defined the path of the read component as /admin/dashboard, which is the same address as the Homepage component. This will make sure the Read component always loads by default.

In the Read component, we want to load all of the available posts. We are also going to add Update and Delete options to each post. Clicking on these options will lead to the update and delete views respectively.

Open the Read.vue file and paste the following:

1<!-- File: ./resources/app/js/components/Read.vue -->
2    <template>
3        <div id="posts">
4            <p class="border p-3" v-for="post in posts">
5                {{ post.title }}
6                <router-link :to="{ name: 'update', params: { postId : post.id } }">
7                    <button type="button" class="p-1 mx-3 float-right btn btn-light">
8                        Update
9                    </button>
10                </router-link>
11                <button 
12                    type="button" 
13                    @click="deletePost(post.id)" 
14                    class="p-1 mx-3 float-right btn btn-danger"
15                >
16                    Delete
17                </button>
18            </p>
19            <div>
20                <button 
21                    v-if="next" 
22                    type="button" 
23                    @click="navigate(next)" 
24                    class="m-3 btn btn-primary"
25                >
26                  Next
27                </button>
28                <button 
29                    v-if="prev" 
30                    type="button" 
31                    @click="navigate(prev)" 
32                    class="m-3 btn btn-primary"
33                >
34                  Previous
35                </button>
36            </div>
37        </div>
38    </template>

Above, we have the template to handle the posts that are loaded from the API. Next, paste the following below the closing template tag:

1<script>
2    export default {
3      mounted() {
4        this.getPosts();
5      },
6      data() {
7        return {
8          posts: {},
9          next: null,
10          prev: null
11        };
12      },
13      methods: {
14        getPosts(address) {
15          axios.get(address ? address : "/api/posts").then(response => {
16            this.posts = response.data.data;
17            this.prev = response.data.links.prev;
18            this.next = response.data.links.next;
19          });
20        },
21        deletePost(id) {
22          axios.delete("/api/posts/" + id).then(response => this.getPosts())
23        },
24        navigate(address) {
25          this.getPosts(address)
26        }
27      }
28    };
29    </script>

In the script above, we defined a getPosts() method that requests a list of posts from the backend server. We also defined a posts object as a data property. This object will be populated whenever posts are received from the backend server.

We defined next and prev data string properties to store pagination links and only display the pagination options where it is available.

Lastly, we defined a deletePost() method that takes the id of a post as a parameter and sends a DELETE request to the API interface using Axios.

Testing the application

Now that we have completed the first few components, we can serve the application using this command:

    $ php artisan serve

We will also build the assets so that our JavaScript is compiled for us. To do this, will run the command below in the root of the project folder:

    $ npm run dev

We can visit the application’s URL http://localhost:8000 and log in as an admin user, and delete a post:

laravel-vue-cms-demo-part-4

Conclusion

In this part of the series, we started building the admin dashboard using Vue. We installed VueRouter to make the admin dashboard a SPA. We added the homepage view of the admin dashboard and included read and delete functionalities.

We are not done with the dashboard just yet. In the next part, we will add the views that lets us create and update posts.

The source code for this project is available here on Github.