Building a Vue SPA with Laravel Part 3

https://laravel-news.com/building-vue-spa-laravel-part-3

e 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:

 1created() { 2    this.fetchData(); 3}, 4methods: { 5    fetchData() { 6        this.error = this.users = null; 7        this.loading = true; 8        axios 9            .get('/api/users')10            .then(response => {11                this.loading = false;12                this.users = response.data;13            }).catch(error => {14                this.loading = false;15                this.error = error.response.data.message || error.message;16            });17    }18}

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:

1php artisan make:seeder UsersTableSeeder

The UsersTableSeeder is pretty simple right now—we just create 50 users with a model factory:

 1<?php 2 3use Illuminate\Database\Seeder; 4 5class UsersTableSeeder extends Seeder 6{ 7    public function run() 8    { 9        factory(App\User::class, 50)->create();10    }11}

Next, let’s add the UsersTableSeeder to our database/seeds/DatabaseSeeder.php file:

 1<?php 2 3use Illuminate\Database\Seeder; 4 5class DatabaseSeeder extends Seeder 6{ 7    /** 8     * Run the database seeds. 9     *10     * @return void11     */12    public function run()13    {14        $this->call([15            UsersTableSeeder::class,16        ]);17    }18}

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):

1mysql -u root -e"create database vue_spa;"23# or you could prompt for the password with the -p flag4mysql -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:

1# Ensure the database seeders get auto-loaded2composer dump-autoload3php 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:

1Route::get('/users', function () {2    return factory('App\User', 10)->make();3});

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:

1php artisan make:controller Api/UsersController2php 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:

1Route::namespace('Api')->group(function () {2    Route::get('/users', 'UsersController@index');3});

The controller is pretty straightforward; we are returning an Eloquent API resource with pagination:

 1<?php 2 3namespace App\Http\Controllers\Api; 4 5use App\User; 6use Illuminate\Http\Request; 7use App\Http\Controllers\Controller; 8use App\Http\Resources\UserResource; 910class UsersController extends Controller11{12    public function index()13    {14        return UserResource::collection(User::paginate(10));15    }16}

Here’s an example of what the JSON response will look like once we wire up the UserResource with API format:

 1{ 2   "data":[ 3      { 4         "name":"Francis Marquardt", 5         "email":"schamberger.adrian@example.net" 6      }, 7      { 8         "name":"Dr. Florine Beatty", 9         "email":"fcummerata@example.org"10      },11      ...12   ],13   "links":{14      "first":"http:\/\/vue-router.test\/api\/users?page=1",15      "last":"http:\/\/vue-router.test\/api\/users?page=5",16      "prev":null,17      "next":"http:\/\/vue-router.test\/api\/users?page=2"18   },19   "meta":{20      "current_page":1,21      "from":1,22      "last_page":5,23      "path":"http:\/\/vue-router.test\/api\/users",24      "per_page":10,25      "to":10,26      "total":5027   }28}

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:

 1<?php 2 3namespace App\Http\Resources; 4 5use Illuminate\Http\Resources\Json\Resource; 6 7class UserResource extends Resource 8{ 9    /**10     * Transform the resource into an array.11     *12     * @param  \Illuminate\Http\Request  $request13     * @return array14     */15    public function toArray($request)16    {17        return [18            'name' => $this->name,19            'email' => $this->email,20        ];21    }22}

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:

1this.users = response.data.data;

Here’s the adjusted fetchData() method that works with our new API:

 1fetchData() { 2    this.error = this.users = null; 3    this.loading = true; 4    axios 5        .get('/api/users') 6        .then(response => { 7            this.loading = false; 8            this.users = response.data.data; 9        }).catch(error => {10            this.loading = false;11            this.error = error.response.data.message || error.message;12        });13}

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:

1beforeRouteEnter (to, from, next) {2    getPost(to.params.id, (err, post) => {3      next(vm => vm.setData(err, post))4    })5  },

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:

 1const getUsers = (page, callback) => { 2    const params = { page }; 3 4    axios 5        .get('/api/users', { params }) 6        .then(response => { 7            callback(null, response.data); 8        }).catch(error => { 9            callback(error, error.response.data);10        });11};

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:

1{2    params: {3        page: 14    }5}

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():

1beforeRouteEnter (to, from, next) {2    const params = {3        page: to.query.page4    };56    getUsers(to.query.page, (err, data) => {7        next(vm => vm.setData(err, data));8    });9},

This piece is the callback argument in the getUsers() call after the data is returned from the API:

1(err, data) => {2    next(vm => vm.setData(err, data));3}

Which is then called like this in getUsers() on a successful response from the API:

1callback(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:

1// when route changes and this component is already rendered,2// the logic will be slightly different.3beforeRouteUpdate (to, from, next) {4    this.users = this.links = this.meta = null5    getUsers(to.query.page, (err, data) => {6        this.setData(err, data);7        next();8    });9},

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:

1setData(err, { data: users, links, meta }) {2    if (err) {3        this.error = err.toString();4    } else {5        this.users = users;6        this.links = links;7        this.meta = meta;8    }9},

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:

  1<template>  2    <div class="users">  3        <div v-if="error" class="error">  4            <p>{{ error }}</p>  5        </div>  6  7        <ul v-if="users">  8            <li v-for="{ id, name, email } in users">  9                <strong>Name:</strong> {{ name }}, 10                <strong>Email:</strong> {{ email }} 11            </li> 12        </ul> 13 14        <div class="pagination"> 15            <button :disabled="! prevPage" @click.prevent="goToPrev">Previous</button> 16            {{ paginatonCount }} 17            <button :disabled="! nextPage" @click.prevent="goToNext">Next</button> 18        </div> 19    </div> 20</template> 21<script> 22import axios from 'axios'; 23 24const getUsers = (page, callback) => { 25    const params = { page }; 26 27    axios 28        .get('/api/users', { params }) 29        .then(response => { 30            callback(null, response.data); 31        }).catch(error => { 32            callback(error, error.response.data); 33        }); 34}; 35 36export default { 37    data() { 38        return { 39            users: null, 40            meta: null, 41            links: { 42                first: null, 43                last: null, 44                next: null, 45                prev: null, 46            }, 47            error: null, 48        }; 49    }, 50    computed: { 51        nextPage() { 52            if (! this.meta || this.meta.current_page === this.meta.last_page) { 53                return; 54            } 55 56            return this.meta.current_page + 1; 57        }, 58        prevPage() { 59            if (! this.meta || this.meta.current_page === 1) { 60                return; 61            } 62 63            return this.meta.current_page - 1; 64        }, 65        paginatonCount() { 66            if (! this.meta) { 67                return; 68            } 69 70            const { current_page, last_page } = this.meta; 71 72            return `${current_page} of ${last_page}`; 73        }, 74    }, 75    beforeRouteEnter (to, from, next) { 76        getUsers(to.query.page, (err, data) => { 77            next(vm => vm.setData(err, data)); 78        }); 79    }, 80    // when route changes and this component is already rendered, 81    // the logic will be slightly different. 82    beforeRouteUpdate (to, from, next) { 83        this.users = this.links = this.meta = null 84        getUsers(to.query.page, (err, data) => { 85            this.setData(err, data); 86            next(); 87        }); 88    }, 89    methods: { 90        goToNext() { 91            this.$router.push({ 92                query: { 93                    page: this.nextPage, 94                }, 95            }); 96        }, 97        goToPrev() { 98            this.$router.push({ 99                name: 'users.index',100                query: {101                    page: this.prevPage,102                }103            });104        },105        setData(err, { data: users, links, meta }) {106            if (err) {107                this.error = err.toString();108            } else {109                this.users = users;110                this.links = links;111                this.meta = meta;112            }113        },114    }115}116</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:

1this.$router.push({2    query: {3        page: `${this.nextPage}`,4    },5});

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:

 1# NPM 2npm run dev 3 4# Watch to update automatically while developing 5npm run watch 6 7# Yarn 8yarn dev 910# Watch to update automatically while developing11yarn 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.

Last updated