Building a Vue SPA with Laravel Part 3

Published on by

Building a Vue SPA with Laravel Part 3 image

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 flag
mysql -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-loaded
composer dump-autoload
php 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/UsersController
php 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:

# NPM
npm run dev
 
# Watch to update automatically while developing
npm run watch
 
# Yarn
yarn dev
 
# Watch to update automatically while developing
yarn 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.

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate logo

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate

Build your SaaS application in hours. Out-of-the-box multi-tenancy and seamless Stripe integration. Supports subscriptions and one-time purchases, allowing you to focus on building and creating without repetitive setup tasks.

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector
MongoDB logo

MongoDB

Enhance your PHP applications with the powerful integration of MongoDB and Laravel, empowering developers to build applications with ease and efficiency. Support transactional, search, analytics and mobile use cases while using the familiar Eloquent APIs. Discover how MongoDB's flexible, modern database can transform your Laravel applications.

MongoDB

The latest

View all →
Asymmetric Property Visibility in PHP 8.4 image

Asymmetric Property Visibility in PHP 8.4

Read article
Access Laravel Pulse Data as a JSON API image

Access Laravel Pulse Data as a JSON API

Read article
Laravel Forge adds Statamic Integration image

Laravel Forge adds Statamic Integration

Read article
Transform Data into Type-safe DTOs with this PHP Package image

Transform Data into Type-safe DTOs with this PHP Package

Read article
PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell image

PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell

Read article
Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3 image

Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3

Read article