The previous article had us building a JSON Rest API using the Laravel Framework. In this tutorial, we’ll use Vuejs to build a front end which can consume the API we have in place. Instead of rendering blade files, we can use Vue components and AJAX to simply fetch the data we need to display from the API. We’ll even learn a little bit about customizing the CSS in Laravel using SCSS and Laravel Mix.
Installing Dependencies
To kick things off we need to install the dependencies specified in the package.json file of our Laravel instance. To do this just run npm install
in the project root like so.
This will download and install all of the software needed for the front end side of development.
Laravel Mix
Laravel Mix is one of the dependencies that is now installed. Webpack is really hard. Laravel Mix makes it easier. With Mix and SCSS we can quickly change the look of the site. Before running Mix, we need to understand what it is going to do for us. So by default, Mix follows the logic found in webpack.mix.js
which is in the root directory of a Laravel project.
const mix = require('laravel-mix');
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|
*/
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css');
What does this mean however? Well, when you run npm run dev
for example, Mix is going to look at the contents of resources/js/app.js and resources/sass/app.scss. It will then compile these raw assets into usable code placed in public/js and public/css.
How Do I Customize My Styles?
Let’s say you want to try the cool Minty Bootswatch theme on a Laravel Project. How can we do that with Mix? Very easy! Download the _variables.scss file from Bootswatch site and replace the one found in /resources/sass/_variables.scss
. Now we see the original and the new file.
Original _variables.scss
// Body
$body-bg: #f8fafc;
// Typography
$font-family-sans-serif: 'Nunito', sans-serif;
$font-size-base: 0.9rem;
$line-height-base: 1.6;
// Colors
$blue: #3490dc;
$indigo: #6574cd;
$purple: #9561e2;
$pink: #f66d9b;
$red: #e3342f;
$orange: #f6993f;
$yellow: #ffed4a;
$green: #38c172;
$teal: #4dc0b5;
$cyan: #6cb2eb;
Minty version _variables.scss
// Minty 4.3.1
// Bootswatch
//
// Color system
//
$white: #fff !default;
$gray-100: #f8f9fa !default;
$gray-200: #f7f7f9 !default;
$gray-300: #eceeef !default;
$gray-400: #ced4da !default;
$gray-500: #aaa !default;
$gray-600: #888 !default;
$gray-700: #5a5a5a !default;
$gray-800: #343a40 !default;
$gray-900: #212529 !default;
$black: #000 !default;
$blue: #007bff !default;
$indigo: #6610f2 !default;
$purple: #6f42c1 !default;
$pink: #e83e8c !default;
$red: #FF7851 !default;
$orange: #fd7e14 !default;
$yellow: #FFCE67 !default;
$green: #56CC9D !default;
$teal: #20c997 !default;
$cyan: #6CC3D5 !default;
$primary: #78C2AD !default;
$secondary: #F3969A !default;
$success: $green !default;
$info: $cyan !default;
$warning: $yellow !default;
$danger: $red !default;
$light: $gray-100 !default;
$dark: $gray-800 !default;
$yiq-contrasted-threshold: 250 !default;
// Body
$body-color: $gray-600 !default;
// Components
$border-radius: .4rem !default;
$border-radius-lg: .6rem !default;
$border-radius-sm: .3rem !default;
// Fonts
$headings-font-family: "Montserrat", -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !default;
$headings-color: $gray-700 !default;
// Tables
$table-border-color: rgba(0,0,0,0.05) !default;
// Dropdowns
$dropdown-link-hover-color: $white !default;
$dropdown-link-hover-bg: $secondary !default;
// Navbar
$navbar-dark-color: rgba($white,.6) !default;
$navbar-dark-hover-color: $white !default;
$navbar-light-color: rgba($black,.3) !default;
$navbar-light-hover-color: $gray-700 !default;
$navbar-light-active-color: $gray-700 !default;
$navbar-light-disabled-color: rgba($black,.1) !default;
// Pagination
$pagination-color: $white !default;
$pagination-bg: $primary !default;
$pagination-border-color: $primary !default;
$pagination-hover-color: $white !default;
$pagination-hover-bg: $secondary !default;
$pagination-hover-border-color: $pagination-hover-bg !default;
$pagination-active-bg: $secondary !default;
$pagination-active-border-color: $pagination-active-bg !default;
$pagination-disabled-color: $white !default;
$pagination-disabled-bg: #CCE8E0 !default;
$pagination-disabled-border-color: $pagination-disabled-bg !default;
// Breadcrumbs
$breadcrumb-bg: $primary !default;
$breadcrumb-divider-color: $white !default;
$breadcrumb-active-color: $breadcrumb-divider-color !default;
This file sets values for all of these variables giving Bootstrap an entirely new look. Now we can modify the welcome.blade.php file like so just for a test. We want to test different Bootstrap classes to see if the new effect has taken place.
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<!-- Styles -->
<link rel="stylesheet" href="{{ asset('/css/app.css') }}">
</head>
<body class="container">
<div class="jumbotron mt-3">
<h1 class="display-3">Hello, world!</h1>
<p class="lead">This is a simple hero unit!</p>
<hr class="my-4">
<p>It uses utility classes for typography and spacing to space content out within the larger container.</p>
<button type="button" class="btn btn-primary">Primary</button>
<button type="button" class="btn btn-secondary">Secondary</button>
<button type="button" class="btn btn-success">Success</button>
<button type="button" class="btn btn-info">Info</button>
<button type="button" class="btn btn-warning">Warning</button>
<button type="button" class="btn btn-danger">Danger</button>
<button type="button" class="btn btn-link">Link</button>
</div>
<script src="{{ asset('js/app.js') }}"></script>
</body>
</html>
Run Mix
We can now run mix with npm run dev
and there should be a result similar to this.
Note: If you run into errors when running mix, see this post which should help.
Now instead of the standard splash screen, we see the new styles applied. Cool!
Building A Vue Front End
With that little bit of setup and configuration out of the way, we can now start building out the Vue Front end for our API. Just a couple of components might be good for this. We’ll have a simple Navbar component Component and a PostList Component. To start, we’ll just scaffold things out then add dynamic data as we go.
welcome.blade.php
Laravel is still going to load this view file as the home page. On that home page, is a div with the id of “app”. Our Vue application will attach or mount itself to that div. Here is the beginnings of that file.
<!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" content="{{ csrf_token() }}">
<script>window.Laravel = {csrfToken: '{{ csrf_token() }}'}</script>
<title>Vue Front End For A Laravel API</title>
<link rel="stylesheet" href="{{ asset('/css/app.css') }}">
</head>
<body class="container-fluid">
<div id="app">
<navbar></navbar>
</div>
<script src="{{ asset('js/app.js') }}"></script>
</body>
</html>
Note the inclusion of the csrf_token fields. Without those you will get errors of “CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token” in the console so be sure to include those lines of code.
Adding A Navbar Component
You’ll notice that on line 13 above there is a reference to a <navbar> component. We need to build and register that component in order for it to display. We can navigate to the resources/js/components
directory and create a Navbar.vue file. This file has a special .vue
extension which means a couple of things. The first is that it is a Vue single file component. These types of files allow you to put the template, script, and even custom css in the same file. You then build the file via Webpack (in this case Laravel Mix), and you get a working result. Pretty cool!
resources/js/components/Navbar.vue
<template>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<a class="navbar-brand" href="#">Minty Fresh</a>
</nav>
</template>
Now, we register the component in resources/js/app.js
.
resources/js/app.js
require('./bootstrap');
window.Vue = require('vue');
Vue.component('navbar', require('./components/Navbar.vue').default);
const app = new Vue({
el: '#app'
});
As long as you have set up Laravel Mix to watch your files now by either running npm run watch
or npm run watch-poll
, then you should be able to visit the homepage and see a Minty Navbar.
Listing The Posts
To list some some posts, we can add a new component named PostList.vue in resources/js/components
. So first off, go ahead and add that file like we see here.
The new component needs to be registered in the app.js file.
resources/js/app.js
require('./bootstrap');
window.Vue = require('vue');
Vue.component('navbar', require('./components/Navbar.vue').default);
Vue.component('post-list', require('./components/PostList.vue').default);
const app = new Vue({
el: '#app'
});
Now in the PostList.vue file, we need to populate the template and script areas. We make use of the created Lifecycle Hook. This function is automatically called right after the instance for the component has been created. This is right before the component is mounted into the page, so its a perfect time to fetch the data from the API that we’ll need using the JavaScript Fetch api.
resources/js/components/PostList.vue
<template>
<div>
<div class="card mb-2" v-for="post in posts" v-bind:key="post.id">
<div class="card-body ">
<h4 class="card-title">{{ post.title }}</h4>
<p class="card-text">{{ post.body }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
posts: []
};
},
created() {
this.getPosts();
},
methods: {
getPosts(api_url) {
api_url = api_url || '/api/posts';
fetch(api_url)
.then(response => response.json())
.then(response => {
this.posts = response.data;
})
.catch(err => console.log(err));
}
}
};
</script>
In the code above the posts
array on line 16 is what will hold the api response containing all the posts. This is populated when the page loads and the created()
hook runs. When that happens the getPosts()
function is called which is what fetches the data from the api and assigns it to the posts
variable. With the data in place, the template section uses your standard Vue v-for to create a list of posts.
Adding A Paginator
We got 5 posts to successfully display above, and it looks pretty cool! Recall that our API does provide information about the previous, next, and current links. This means you can use that data to create a paginator. The highlighted lines below show the additions to display the paginator.
<template>
<div>
<nav>
<ul class="pagination justify-content-center">
<li v-bind:class="[{disabled: !pagination.prev_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.prev_page_url)">Previous</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#">{{ pagination.current_page }} of {{ pagination.last_page }}</a>
</li>
<li v-bind:class="[{disabled: !pagination.next_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.next_page_url)">Next</a>
</li>
</ul>
</nav>
<div class="card mb-2" v-for="post in posts" v-bind:key="post.id">
<div class="card-body ">
<h4 class="card-title">{{ post.title }}</h4>
<p class="card-text">{{ post.body }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
posts: [],
pagination: {}
};
},
created() {
this.getPosts();
},
methods: {
getPosts(api_url) {
let vm = this;
api_url = api_url || '/api/posts';
fetch(api_url)
.then(response => response.json())
.then(response => {
this.posts = response.data;
vm.paginator(response.meta, response.links);
})
.catch(err => console.log(err));
},
paginator(meta, links) {
this.pagination = {
current_page: meta.current_page,
last_page: meta.last_page,
next_page_url: links.next,
prev_page_url: links.prev
};
},
}
};
</script>
This results in the paginator being displayed which tells us what page we are on and dynamically enables or disables the previous and next buttons based on where in the set we are located.
Add A Post
Let’s add a form to the page so a post can be added by sending a POST request to the API. On the back end, we’ll first change the pagination to 3 per page to give us some more room.
<template>
<div>
<form @submit.prevent="addPost">
<div class="form-group">
<input type="text" class="form-control" placeholder="Title" v-model="post.title">
</div>
<div class="form-group">
<textarea class="form-control" placeholder="Body" v-model="post.body"></textarea>
</div>
<button type="submit" class="btn btn-success">Save</button>
</form>
<nav>
<ul class="pagination justify-content-center">
<li v-bind:class="[{disabled: !pagination.prev_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.prev_page_url)">Previous</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#">{{ pagination.current_page }} of {{ pagination.last_page }}</a>
</li>
<li v-bind:class="[{disabled: !pagination.next_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.next_page_url)">Next</a>
</li>
</ul>
</nav>
<div class="card mb-2" v-for="post in posts" v-bind:key="post.id">
<div class="card-body ">
<h4 class="card-title">{{ post.title }}</h4>
<p class="card-text">{{ post.body }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
posts: [],
pagination: {},
post: {
id: '',
title: '',
body: ''
}
};
},
created() {
this.getPosts();
},
methods: {
getPosts(api_url) {
let vm = this;
api_url = api_url || '/api/posts';
fetch(api_url)
.then(response => response.json())
.then(response => {
this.posts = response.data;
vm.paginator(response.meta, response.links);
})
.catch(err => console.log(err));
},
paginator(meta, links) {
this.pagination = {
current_page: meta.current_page,
last_page: meta.last_page,
next_page_url: links.next,
prev_page_url: links.prev
};
},
addPost() {
fetch('api/post', {
method: 'post',
body: JSON.stringify(this.post),
headers: {
'content-type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
this.getPosts();
})
.catch(err => console.log(err));
}
}
};
</script>
Now we have the ability to add a new post which is great 🙂
Delete A Post
To delete a post we can add a new function to the methods object named deletePost(). It accepts the id of the post to delete, then makes an ajax delete request to the api. The highlights below show the new code to allow for deleting a post.
<template>
<div>
<form @submit.prevent="addPost">
<div class="form-group">
<input type="text" class="form-control" placeholder="Title" v-model="post.title">
</div>
<div class="form-group">
<textarea class="form-control" placeholder="Body" v-model="post.body"></textarea>
</div>
<button type="submit" class="btn btn-success">Save</button>
</form>
<nav>
<ul class="pagination justify-content-center">
<li v-bind:class="[{disabled: !pagination.prev_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.prev_page_url)">Previous</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#">{{ pagination.current_page }} of {{ pagination.last_page }}</a>
</li>
<li v-bind:class="[{disabled: !pagination.next_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.next_page_url)">Next</a>
</li>
</ul>
</nav>
<div class="card mb-2" v-for="post in posts" v-bind:key="post.id">
<div class="card-body ">
<h4 class="card-title">{{ post.title }}</h4>
<p class="card-text">{{ post.body }}</p>
<button type="button" @click="deletePost(post.id)" class="btn btn-secondary">Delete</button>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
posts: [],
pagination: {},
post: {
id: '',
title: '',
body: ''
}
};
},
created() {
this.getPosts();
},
methods: {
getPosts(api_url) {
let vm = this;
api_url = api_url || '/api/posts';
fetch(api_url)
.then(response => response.json())
.then(response => {
this.posts = response.data;
vm.paginator(response.meta, response.links);
})
.catch(err => console.log(err));
},
paginator(meta, links) {
this.pagination = {
current_page: meta.current_page,
last_page: meta.last_page,
next_page_url: links.next,
prev_page_url: links.prev
};
},
addPost() {
fetch('api/post', {
method: 'post',
body: JSON.stringify(this.post),
headers: {
'content-type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
this.getPosts();
})
.catch(err => console.log(err));
},
deletePost(id) {
fetch('api/post/' + id, {
method: 'delete'
})
.then(response => response.json())
.then(data => {
this.getPosts();
})
.catch(err => console.log(err));
},
}
};
</script>
When a post is deleted, the page automatically refreshes.
Update A Post
We can add a new button to each post which will give the option to update that post. This will be a kind of two step process. The first step is to click the update button to load the post data into the form. Then we can make changes to the data in the form, and finally click save. That means the addPost() function will need new logic to account for that. First though, let’s see how to add the button to allow updating by loading the right data into the form.
<template>
<div>
<form @submit.prevent="addPost">
<div class="form-group">
<input type="text" class="form-control" placeholder="Title" v-model="post.title">
</div>
<div class="form-group">
<textarea class="form-control" placeholder="Body" v-model="post.body"></textarea>
</div>
<button type="submit" class="btn btn-success">Save</button>
</form>
<nav>
<ul class="pagination justify-content-center">
<li v-bind:class="[{disabled: !pagination.prev_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.prev_page_url)">Previous</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#">{{ pagination.current_page }} of {{ pagination.last_page }}</a>
</li>
<li v-bind:class="[{disabled: !pagination.next_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.next_page_url)">Next</a>
</li>
</ul>
</nav>
<div class="card mb-2" v-for="post in posts" v-bind:key="post.id">
<div class="card-body ">
<h4 class="card-title">{{ post.title }}</h4>
<p class="card-text">{{ post.body }}</p>
<button type="button" @click="deletePost(post.id)" class="btn btn-secondary">Delete</button>
<button type="button" @click="updatePost(post)" class="btn btn-success">Update</button>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
posts: [],
pagination: {},
post: {
id: '',
title: '',
body: ''
},
update: false,
post_id: ''
};
},
created() {
this.getPosts();
},
methods: {
getPosts(api_url) {
let vm = this;
api_url = api_url || '/api/posts';
fetch(api_url)
.then(response => response.json())
.then(response => {
this.posts = response.data;
vm.paginator(response.meta, response.links);
})
.catch(err => console.log(err));
},
paginator(meta, links) {
this.pagination = {
current_page: meta.current_page,
last_page: meta.last_page,
next_page_url: links.next,
prev_page_url: links.prev
};
},
addPost() {
fetch('api/post', {
method: 'post',
body: JSON.stringify(this.post),
headers: {
'content-type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
this.getPosts();
})
.catch(err => console.log(err));
},
deletePost(id) {
fetch('api/post/' + id, {
method: 'delete'
})
.then(response => response.json())
.then(data => {
this.getPosts();
})
.catch(err => console.log(err));
},
updatePost(post) {
this.update = true;
this.post.id = post.id;
this.post.post_id = post.id;
this.post.title = post.title;
this.post.body = post.body;
}
}
};
</script>
Depending on which post you click the update button for, that data is loaded into the form so we can take action on it. So if you wanted to update post 2, you can click that particular update button, change the data, then save.
Modify The addPost() Function
In the section just above, clicking on the update button for a given post loads up the data for that post into the form. Clicking on Save for the form however would currently create a new post, not update the one just loaded into the form. We can use some conditional logic in the addPost() function to fix this. We can also add a clearForm() function for two purposes. The first is to allow the user to clear out the form if they decide not to do an update, and secondly to automatically clear the form once a new post is added.
<template>
<div>
<form @submit.prevent="addPost">
<div class="form-group">
<input type="text" class="form-control" placeholder="Title" v-model="post.title">
</div>
<div class="form-group">
<textarea class="form-control" placeholder="Body" v-model="post.body"></textarea>
</div>
<button type="submit" class="btn btn-success">Save</button>
<button @click.prevent="clearForm()" class="btn btn-warning">Clear Form</button>
</form>
<nav>
<ul class="pagination justify-content-center">
<li v-bind:class="[{disabled: !pagination.prev_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.prev_page_url)">Previous</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#">{{ pagination.current_page }} of {{ pagination.last_page }}</a>
</li>
<li v-bind:class="[{disabled: !pagination.next_page_url}]" class="page-item">
<a class="page-link" href="#" @click="getPosts(pagination.next_page_url)">Next</a>
</li>
</ul>
</nav>
<div class="card mb-2" v-for="post in posts" v-bind:key="post.id">
<div class="card-body ">
<h4 class="card-title">{{ post.title }}</h4>
<p class="card-text">{{ post.body }}</p>
<button type="button" @click="deletePost(post.id)" class="btn btn-secondary">Delete</button>
<button type="button" @click="updatePost(post)" class="btn btn-success">Update</button>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
posts: [],
pagination: {},
post: {
id: '',
title: '',
body: ''
},
update: false,
post_id: ''
};
},
created() {
this.getPosts();
},
methods: {
getPosts(api_url) {
let vm = this;
api_url = api_url || '/api/posts';
fetch(api_url)
.then(response => response.json())
.then(response => {
this.posts = response.data;
vm.paginator(response.meta, response.links);
})
.catch(err => console.log(err));
},
paginator(meta, links) {
this.pagination = {
current_page: meta.current_page,
last_page: meta.last_page,
next_page_url: links.next,
prev_page_url: links.prev
};
},
addPost() {
if (this.update === false) {
fetch('api/post', {
method: 'post',
body: JSON.stringify(this.post),
headers: {
'content-type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
this.clearForm();
this.getPosts();
})
.catch(err => console.log(err));
} else {
fetch('api/post', {
method: 'put',
body: JSON.stringify(this.post),
headers: {
'content-type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
this.clearForm();
this.getPosts();
})
.catch(err => console.log(err));
}
},
deletePost(id) {
fetch('api/post/' + id, {
method: 'delete'
})
.then(response => response.json())
.then(data => {
this.getPosts();
})
.catch(err => console.log(err));
},
updatePost(post) {
this.update = true;
this.post.id = post.id;
this.post.post_id = post.id;
this.post.title = post.title;
this.post.body = post.body;
},
clearForm() {
this.update = false;
this.post.id = null;
this.post.post_id = null;
this.post.title = '';
this.post.body = '';
}
}
};
</script>
So for the final result we can do the full create, read, update, and delete of Posts using Vue on the front end and Laravel on the back end.
Learn More
- API Driven Development With Laravel and VueJS – Server Side Up
- Build a Basic CRUD App with Laravel and Vue | Okta Developer
- Build a modern web application with Laravel and Vue – Pusher Blog
- Vue Laravel CRUD Example Tutorial From Scratch – AppDividend
Building A Vue Front End For A Laravel API Summary
With that, we now have a Vue front end for the Laravel API Resource we had built in the prior tutorial. We built this right inside of Laravel using the Mix tool to enable a build process which compiled the .vue files to production ready JavaScript.