Building a Vue SPA with Laravel Part 3
Published on by Paul Redmond
We will continue building our Vue SPA with Laravel by showing you how to load asynchronous data before the vue-router
enters a route.
We left off in Building a Vue SPA With Laravel Part 2 finishing a UsersIndex
Vue component which loads users from an API asynchronously. We skimped on building a real API backed by the database and opted for fake data in the API response from Laravel’s factory()
method.
If you haven’t read Part 1 and Part 2 of building a Vue SPA with Laravel, I suggest you start with those posts first and then come back. I’ll be waiting for you!
In this tutorial we are also going to swap out our fake /users
endpoint with a real one powered by a database. I prefer to use MySQL, but you can use whatever database driver you want!
Our UsersIndex.vue
router component is loading the data from the API during the created()
lifecycle hook. Here’s what our fetchData()
method looks like at the conclusion of Part 2:
created() { this.fetchData();},methods: { fetchData() { this.error = this.users = null; this.loading = true; axios .get('/api/users') .then(response => { this.loading = false; this.users = response.data; }).catch(error => { this.loading = false; this.error = error.response.data.message || error.message; }); }}
I promised that I’d show you how to retrieve data from the API before navigating to a component, but before we do that we need to swap our API out for some real data.
Creating a Real Users Endpoint
We are going to create a UsersController
from which we return JSON data using Laravel’s new API resources introduced in Laravel 5.5.
Before we create the controller and API resource, let’s first set up a database and seeder to provide some test data for our SPA.
The User Database Seeder
We can create a new users seeder with the make:seeder
command:
php artisan make:seeder UsersTableSeeder
The UsersTableSeeder
is pretty simple right now—we just create 50 users with a model factory:
<?php use Illuminate\Database\Seeder; class UsersTableSeeder extends Seeder{ public function run() { factory(App\User::class, 50)->create(); }}
Next, let’s add the UsersTableSeeder
to our database/seeds/DatabaseSeeder.php
file:
<?php use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder{ /** * Run the database seeds. * * @return void */ public function run() { $this->call([ UsersTableSeeder::class, ]); }}
We can’t apply this seeder without first creating and configuring a database.
Configuring a Database
It’s time to hook our Vue SPA Laravel application up to a real database. You can use SQLite with a GUI like TablePlus or MySQL. If you’re new to Laravel, you can go through the extensive documentation on getting started with a database.
If you have a local MySQL instance running on your machine, you can create a new database rather quickly from the command line with the following (assuming you don’t have a password for local development):
mysql -u root -e"create database vue_spa;" # or you could prompt for the password with the -p flagmysql -u root -e"create database vue_spa;" -p
Once you have the database, in the .env
file configure the DB_DATABASE=vue_spa
. If you get stuck, follow the documentation which should make it easy to get your database working.
Once you have the database connection configured, you can migrate your database tables and add seed data. Laravel ships with a Users table migration that we are using to seed data:
# Ensure the database seeders get auto-loadedcomposer dump-autoloadphp artisan migrate:fresh --seed
You can also use the separate artisan db:seed
command if you wish! That’s it; you should have a database with 50 users that we can query and return via the API.
The Users Controller
If you recall from Part 2, the fake /users
endpoint found in the routes/api.php
file looks like this:
Route::get('/users', function () { return factory('App\User', 10)->make();});
Let’s create a controller class, which also gives us the added benefit of being able to use php artisan route:cache
in production, which is not possible with closures. We’ll create both the controller and a User API resource class from the command line:
php artisan make:controller Api/UsersControllerphp artisan make:resource UserResource
The first command is adding the User controller in an Api
folder at app/Http/Controllers/Api
, and the second command adds UserResource to the app/Http/Resources
folder.
Here’s the new routes/api.php
code for our controller and Api
namespace:
Route::namespace('Api')->group(function () { Route::get('/users', 'UsersController@index');});
The controller is pretty straightforward; we are returning an Eloquent API resource with pagination:
<?php namespace App\Http\Controllers\Api; use App\User;use Illuminate\Http\Request;use App\Http\Controllers\Controller;use App\Http\Resources\UserResource; class UsersController extends Controller{ public function index() { return UserResource::collection(User::paginate(10)); }}
Here’s an example of what the JSON response will look like once we wire up the UserResource
with API format:
{ "data":[ { "name":"Francis Marquardt", "email":"schamberger.adrian@example.net" }, { "name":"Dr. Florine Beatty", "email":"fcummerata@example.org" }, ... ], "links":{ "first":"http:\/\/vue-router.test\/api\/users?page=1", "last":"http:\/\/vue-router.test\/api\/users?page=5", "prev":null, "next":"http:\/\/vue-router.test\/api\/users?page=2" }, "meta":{ "current_page":1, "from":1, "last_page":5, "path":"http:\/\/vue-router.test\/api\/users", "per_page":10, "to":10, "total":50 }}
It’s fantastic that Laravel provides us with the pagination data and adds the users to a data
key automatically!
Here’s the UserResource
class:
<?php namespace App\Http\Resources; use Illuminate\Http\Resources\Json\Resource; class UserResource extends Resource{ /** * Transform the resource into an array. * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return [ 'name' => $this->name, 'email' => $this->email, ]; }}
The UserResource
transforms each User
model in the collection to an array and provides the UserResource::collection()
method to transform a collection of users into a JSON format.
At this point, you should have a working /api/users
endpoint that we can use with our SPA, but if you are following along, you will notice that our new response format breaks the component.
Fixing the UsersIndex Component
We can quickly get our UsersIndex.vue
Component working again by adjusting the then()
call to reference the data
key where our user data now lives. It might look at little funky at first, but response.data
is the response object, so the user data can be set like the following:
this.users = response.data.data;
Here’s the adjusted fetchData()
method that works with our new API:
fetchData() { this.error = this.users = null; this.loading = true; axios .get('/api/users') .then(response => { this.loading = false; this.users = response.data.data; }).catch(error => { this.loading = false; this.error = error.response.data.message || error.message; });}
Fetching Data Before Navigation
Our component is working with our new API, and it’s an excellent time to demonstrate how you might fetch users before navigation to the component occurs.
With this approach, we fetch the data and then navigate to the new route. We can accomplish this by using the beforeRouteEnter
guard on the incoming component. An example from the vue-router documentation looks like this:
beforeRouteEnter (to, from, next) { getPost(to.params.id, (err, post) => { next(vm => vm.setData(err, post)) }) },
Check the documentation for the complete example, but suffice it to say that we will asynchronously get the user data, once complete, and only after completion, we trigger next()
and set the data on our component (the vm
variable).
Here’s what a getUsers
function might look like to asynchronously get users from the API and then trigger a callback into the component:
const getUsers = (page, callback) => { const params = { page }; axios .get('/api/users', { params }) .then(response => { callback(null, response.data); }).catch(error => { callback(error, error.response.data); });};
Note that the method doesn’t return a Promise, but instead triggers a callback on completion or failure. The callback passes two arguments: an error and the response from the API call.
Our getUsers()
method accepts a page
variable which ends up in the request as a query string param. If it’s null (no page passed in the route), then the API will automatically assume page=1
.
The last thing I’ll point out is the const params
value. It will effectively look like this:
{ params: { page: 1 }}
And here’s how our beforeRouteEnter
guard uses the getUsers
function to get async data and then set it on the component while calling next()
:
beforeRouteEnter (to, from, next) { const params = { page: to.query.page }; getUsers(to.query.page, (err, data) => { next(vm => vm.setData(err, data)); });},
This piece is the callback
argument in the getUsers()
call after the data is returned from the API:
(err, data) => { next(vm => vm.setData(err, data));}
Which is then called like this in getUsers()
on a successful response from the API:
callback(null, response.data);
The beforeRouteUpdate
When the component is in a rendered state already, and the route changes, the beforeRouteUpdate
gets called, and Vue reuses the component in the new route. For example, when our users navigate from /users?page=2
to /users?page=3
.
The beforeRouteUpdate
call is similar to beforeRouteEnter
. However, the former has access to this
on the component, so the style is slightly different:
// when route changes and this component is already rendered,// the logic will be slightly different.beforeRouteUpdate (to, from, next) { this.users = this.links = this.meta = null getUsers(to.query.page, (err, data) => { this.setData(err, data); next(); });},
Since the component is in a rendered state, we need to reset a few data properties before getting the next set of users from the API. We have access to the component. Therefore, we can call this.setData()
(which I have yet to show you) first, and then call next()
without a callback.
Finally, here’s the setData
method on the UsersIndex
component:
setData(err, { data: users, links, meta }) { if (err) { this.error = err.toString(); } else { this.users = users; this.links = links; this.meta = meta; }},
The setData()
method uses object destructuring to get the data
, links
and meta
keys coming from the API response. We use the data: users
to assign data
to the new variable named users
for clarity.
Tying the UsersIndex All Together
I’ve shown you pieces of the UsersIndex
component, and we are ready to tie it all together, and sprinkle on some very basic pagination. This tutorial isn’t showing you how to build pagination, so you can find (or create) fancy pagination of your own!
Pagination is an excellent way to show you how to navigate around an SPA with vue-router
programmatically.
Here’s the full component with our new hooks and methods to get async data using router hooks:
<template> <div class="users"> <div v-if="error" class="error"> <p>{{ error }}</p> </div> <ul v-if="users"> <li v-for="{ id, name, email } in users"> <strong>Name:</strong> {{ name }}, <strong>Email:</strong> {{ email }} </li> </ul> <div class="pagination"> <button :disabled="! prevPage" @click.prevent="goToPrev">Previous</button> {{ paginatonCount }} <button :disabled="! nextPage" @click.prevent="goToNext">Next</button> </div> </div></template><script>import axios from 'axios'; const getUsers = (page, callback) => { const params = { page }; axios .get('/api/users', { params }) .then(response => { callback(null, response.data); }).catch(error => { callback(error, error.response.data); });}; export default { data() { return { users: null, meta: null, links: { first: null, last: null, next: null, prev: null, }, error: null, }; }, computed: { nextPage() { if (! this.meta || this.meta.current_page === this.meta.last_page) { return; } return this.meta.current_page + 1; }, prevPage() { if (! this.meta || this.meta.current_page === 1) { return; } return this.meta.current_page - 1; }, paginatonCount() { if (! this.meta) { return; } const { current_page, last_page } = this.meta; return `${current_page} of ${last_page}`; }, }, beforeRouteEnter (to, from, next) { getUsers(to.query.page, (err, data) => { next(vm => vm.setData(err, data)); }); }, // when route changes and this component is already rendered, // the logic will be slightly different. beforeRouteUpdate (to, from, next) { this.users = this.links = this.meta = null getUsers(to.query.page, (err, data) => { this.setData(err, data); next(); }); }, methods: { goToNext() { this.$router.push({ query: { page: this.nextPage, }, }); }, goToPrev() { this.$router.push({ name: 'users.index', query: { page: this.prevPage, } }); }, setData(err, { data: users, links, meta }) { if (err) { this.error = err.toString(); } else { this.users = users; this.links = links; this.meta = meta; } }, }}</script>
If it’s easier to digest, here’s the UsersIndex.vue as a GitHub Gist.
There are quite a few new things here, so I’ll point out some of the more important points. The goToNext()
and goToPrev()
methods demonstrate how you navigate with vue-router
using this.$router.push
:
this.$router.push({ query: { page: `${this.nextPage}`, },});
We are pushing a new page to the query string which triggers beforeRouteUpdate
. I also want to point out that I’m showing you a <button>
element for the previous and next actions, primarily to demonstrate programmatically navigating with vue-router
, and you would likely use <router-link />
to automatically navigate between paginated routes.
I have introduced three computed properties (nextPage
, prevPage
, and paginatonCount
) to determine the next and previous page numbers, and a paginatonCount
to show a visual count of the current page number and the total page count.
The next and previous buttons use the computed properties to determine if they should be disabled, and the “goTo” methods use these computed properties to push the page
query string param to the next or previous page. The buttons are disabled when a next or previous page is null at the boundaries of the first and last pages.
There’s probably a bit of redundancy in the code, but this component illustrates using vue-router
for fetching data before entering a route!
Don’t forget to make sure you build the latest version of your JavaScript by running Laravel Mix:
# NPMnpm run dev # Watch to update automatically while developingnpm run watch # Yarnyarn dev # Watch to update automatically while developingyarn watch
Finally, here’s what our SPA looks like after we update the complete UsersIndex.vue
component:
What’s Next
We now have a working API with real data from a database, and a simple paginated component which uses Laravel’s API model resources on the backend for simple pagination links and wrapping the data in a data
key.
Next, we will work on creating, editing, and deleting users. A /users
resource would be locked down in a real application, but for now, we are just building CRUD functionality to learn how to work with vue-router
to navigate and pull in data asynchronously.
We could also work on abstracting the axios client code out of the component, but for now, it’s simple, so we’ll leave it in the component until Part 4. Once we add additional API features, we’ll want to create a dedicated module for our HTTP client.
You’re ready to move on to Part 4 – editing existing users.