Laravel Idea for PhpStorm - Full-featured IDE for productive artisans!

Building a Vue SPA with Laravel Part 4

Published on by

Building a Vue SPA with Laravel Part 4 image

We left off building a real users endpoint and learned about a new way to fetch component data with Vue router in part 3. Now we’re ready to move our attention to creating CRUD functionality for our users—this tutorial will focus on editing existing users.

Along with working through our first form, we will get a chance to look at defining a dynamic Vue route. The dynamic part of our route will be the user’s ID which matches his or her database record. For editing a user, the Vue route will look like this:

/users/:id/edit

The dynamic part of this route is the :id parameter, which will depend on the user’s ID. We are going to use the id field from the database, but you could also use a UUID or something else.

The Setup

Before we focus on the Vue component, we need to define a new API endpoint to fetch an individual user, and then later we’ll need to specify another endpoint to perform the update.

Open the routes/api.php routes file and add the following route below the index route that fetches all users:

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

Using Laravel’s implicit route model binding, our controller method is straightforward. Add the following method to the app/Http/Controllers/Api/UsersController.php file:

// app/Http/Controllers/Api/UsersController
 
public function show(User $user)
{
return new UserResource($user);
}

Requesting a user at something like /api/users/1 will return the following JSON response:

{
"data": {
"name": "Antonetta Zemlak",
"email":"znikolaus@example.org"
}
}

Our UserResource from Part 3 needs updated to include the id column, so you should update the app/Http/Resources/UserResource.php file to include the id array key. I’ll paste the entire file from Part 3 here:

<?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 [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
}
}

Now our /api/users and /api/users/{user} routes will respond with the id field, which we need to identify the users in our routes.

Defining the UsersEdit Vue Component

With the show route in place, we can turn our attention to defining the frontend Vue route and the accompanying component. Add the following route definition to the resources/js/app.js routes. Here’s a snippet of importing the UsersEdit component—which we have yet to create—along with the entire route instance:

import UsersEdit from './views/UsersEdit';
 
// ...
 
const router = new VueRouter({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/hello',
name: 'hello',
component: Hello,
},
{
path: '/users',
name: 'users.index',
component: UsersIndex,
},
{
path: '/users/:id/edit',
name: 'users.edit',
component: UsersEdit,
},
],
});

We’ve added the users.edit route to the end of the routes configuration.

Next, we need to create the UsersEdit component at resources/assets/js/views/UsersEdit.vue with the following component code:

<template>
<div>
<form @submit.prevent="onSubmit($event)">
<div class="form-group">
<label for="user_name">Name</label>
<input id="user_name" v-model="user.name" />
</div>
<div class="form-group">
<label for="user_email">Email</label>
<input id="user_email" type="email" v-model="user.email" />
</div>
<div class="form-group">
<button type="submit">Update</button>
</div>
</form>
</div>
</template>
<script>
export default {
data() {
return {
user: {
id: null,
name: "",
email: ""
}
};
},
methods: {
onSubmit(event) {
// @todo form submit event
}
},
created() {
// @todo load user details
}
};
</script>

Let’s focus on the template portion first: we render a <form> around a closing div because soon we’ll need to conditionally show the form after loading the user’s data.

The <form> tag has a placeholder @submit event, and we’ve defined an onSubmit() method handler that takes an event object. The last thing I’ll mention is the v-model attributes on the <input> elements, which maps to accompanying data.users Object literal. We’ve stubbed out the default values for id, name, and email.

At this point if you load up /users/1/edit you’ll see an empty form rendered:

We intend on editing existing users, so our next step is figuring out how to grab the dynamic :id property from the route and loading the user’s data from the UsersEdit.vue component.

Loading User Details with a Dedicated Client

Before we load the user data in the component, we’re going to go on a side-quest to extract the /api/users resource into a dedicated API module that we can use to query all users, individual users, and update users.

First, we’re going to create a new folder and file to house the API modules for our backend. You can create these files in any way you please. We’ll demonstrate from the command line on a `Nix command line:

mkdir -p resources/assets/js/api/
touch resources/assets/js/api/users.js

The users.js component is going to expose some functions we can call to do operations on the /api/users resource. This module is going to be relatively simple, but later can allow you to do any mapping, data manipulation, etc. before or after the API request. This file serves as a repository of reusable API operations:

import axios from 'axios';
 
export default {
all() {
return axios.get('/api/users');
},
find(id) {
return axios.get(`/api/users/${id}`);
},
update(id, data) {
return axios.put(`/api/users/${id}`, data);
},
};

Now we can use the same module to get all users, as well as find and update individual users:

// Get all users
client.all().then((data) => mapData);
 
// Find a user
client.find(userId);

For now, the all() method doesn’t accept any pagination query params, but I’ll leave it up to you to implement pagination and replace what we have on the UsersIndex.vue component with our new all() client function.

Loading the User from the UsersEdit Component

Now that we have a reusable—albeit very basic—API client, we can put it to work to load the user data when the edit page is rendered.

We originally stubbed out a created() function on our component, which is where we’ll request the user’s data now:

// UsersEdit.vue Component
<script>
import api from '../api/users';
 
export default {
// ...
created() {
api.find(this.$route.params.id).then((response) => {
this.loaded = true;
this.user = response.data.data;
});
}
}
</script>

Our created() callback calls the users.js client find() function which returns a promise. In the Promise callback, we set a loaded data property (which we haven’t created yet) and set the this.user data property.

Let’s add the loaded property to our data key and set it to false by default:

data() {
return {
loaded: false,
user: {
id: null,
name: "",
email: ""
}
};
},

Since our component loads up the data inside of created() we’ll show a conditional “loading” message on the component initially:

<div v-if="! loaded">Loading...</div>
<form @submit.prevent="onSubmit($event)" v-else>
<!-- ... -->
</form>

At this point if you refresh the page, the component will briefly flash a Loading... message:

And then the user’s data should populate the form:

The API is very quick, so if you want to verify that the condition is working, you can call setTimeout to delay the setting of the this.user data property:

api.find(this.$route.params.id).then((response) => {
setTimeout(() => {
this.loaded = true;
this.user = response.data.data;
}, 5000);
});

The above timeout will show the loading message for five seconds and then set the loaded and user data properties.

Updating the User

We’re ready to hook up the onSubmit() event handler and update the user via the PUT /api/users/{user} API endpoint.

First, let’s add the onSubmit() code and then we’ll move to the Laravel backend to make the backend perform the update on the database:

onSubmit(event) {
this.saving = true;
 
api.update(this.user.id, {
name: this.user.name,
email: this.user.email,
}).then((response) => {
this.message = 'User updated';
setTimeout(() => this.message = null, 2000);
this.user = response.data.data;
}).catch(error => {
console.log(error)
}).then(_ => this.saving = false);
},

We’ve called the api.update() function with the current user’s ID, and passed the name and email values from the bound form inputs.

We then chain a callback on the Promise object to set the success message and set the updated user data after the API succeeds. After 2000 milliseconds we clear the message which will effectively hide the message in the template.

For now, we are catching any errors and logging them to the console. In the future, we may go back and cover handling errors such as server failure or validation errors, but for now, we’ll skip over this to focus on the success state.

We use this.saving to determine if our component is in the process of updating the user. Our template ensures the submit button is disabled when a save is in progress to avoid double submissions with a bound :disabled property:

<div class="form-group">
<button type="submit" :disabled="saving">Update</button>
</div>

Once the API request is finished, the last thing we’re doing here is setting the this.saving to false by chaining on another then() callback after catch. We need to reset this property to false so the component can submit the form again. Our last then() chain uses the _ underscore variable as a convention you’ll find in some languages indicating that there’s an argument here, but we don’t need to use it. You could also define the short arrow function with empty parenthesis:

.then(() => this.saving = false);

We’ve introduced two new data properties that we need to add to our data() call:

data() {
return {
message: null,
loaded: false,
saving: false,
user: {
id: null,
name: "",
email: ""
}
};
},

Next, let’s update our <template> to show the message when it’s set:

<template>
<div>
<div v-if="message" class="alert">{{ message }}</div>
<div v-if="! loaded">Loading...</div>
<form @submit.prevent="onSubmit($event)" v-else>
<div class="form-group">
<label for="user_name">Name</label>
<input id="user_name" v-model="user.name" />
</div>
<div class="form-group">
<label for="user_email">Email</label>
<input id="user_email" type="email" v-model="user.email" />
</div>
<div class="form-group">
<button type="submit" :disabled="saving">Update</button>
</div>
</form>
</div>
</template>

Finally, let’s add a few styles for the alert message at the bottom of the UsersEdit.vue file:

<style lang="scss" scoped>
$red: lighten(red, 30%);
$darkRed: darken($red, 50%);
.form-group label {
display: block;
}
.alert {
background: $red;
color: $darkRed;
padding: 1rem;
margin-bottom: 1rem;
width: 50%;
border: 1px solid $darkRed;
border-radius: 5px;
}
</style>

We’ve finished updating our frontend component to handle a submitted form and update the template accordingly after the API request succeeds. We now need to turn our attention back to the API to wire it all up.

Updating Users in the API Backend

We’re ready to connect all the dots by defining an update method on our User resource controller. We are going to define necessary validation on the server side. However, we aren’t going to wire it up on the frontend yet.

First, we will define a new route in the routes/api.php file for a PUT /api/users/{user} request:

Route::namespace('Api')->group(function () {
Route::get('/users', 'UsersController@index');
Route::get('/users/{user}', 'UsersController@show');
Route::put('/users/{user}', 'UsersController@update');
});

Next, the UsersController@update method will use the request object to validate the data and return the fields we intend to update. Add the following method to the app/Http/Controllers/Api/UsersController.php file:

public function update(User $user, Request $request)
{
$data = $request->validate([
'name' => 'required',
'email' => 'required|email',
]);
 
$user->update($data);
 
return new UserResource($user);
}

Just like the show() method, we are using the implicit request model binding to load the user from the database. After validating the required fields, we update the user model and return the updated model by creating a new instance of the UserResource class.

A successful request to the backend will return the user’s updated JSON data, which we then, in turn, use to update the this.user property in the Vue component.

{
"data": {
"id": 1,
"name":"Miguel Boyle",
"email":"hirthe.joel@example.org"
}
}

Navigating to the Edit Page

We’ve been requesting the /users/:id/edit page directly, however, we haven’t added it anywhere in the interface. Feel free to try to figure out how to dynamically navigate to the edit page on your own before seeing how I did it.

Here’s how I added the edit link for each user listed on the /users index page in the UsersIndex.vue template we created back in Part 2:

<ul v-if="users">
<li v-for="{ id, name, email } in users">
<strong>Name:</strong> {{ name }},
<strong>Email:</strong> {{ email }} |
<router-link :to="{ name: 'users.edit', params: { id } }">Edit</router-link>
</li>
</ul>

We restructure the user object in our loop to give us the id, name and email properties. We use the <router-link/> component to reference our users.edit named route with the id parameter passed in the params key.

To better visualize the <router-link> properties, here’s the route definition from the app.js file we added earlier:

{
path: '/users/:id/edit',
name: 'users.edit',
component: UsersEdit,
},

If you refresh the app or visit the /users endpoint, you’ll see something like the following:

Putting it All Together

If you edit a user now, the backend should save it and respond with a 200 success if all went well. After the PUT request succeeds you should see the following for two seconds:

Here’s the final UsersEdit.vue component in full for your reference:

<template>
<div>
<div v-if="message" class="alert">{{ message }}</div>
<div v-if="! loaded">Loading...</div>
<form @submit.prevent="onSubmit($event)" v-else>
<div class="form-group">
<label for="user_name">Name</label>
<input id="user_name" v-model="user.name" />
</div>
<div class="form-group">
<label for="user_email">Email</label>
<input id="user_email" type="email" v-model="user.email" />
</div>
<div class="form-group">
<button type="submit" :disabled="saving">Update</button>
</div>
</form>
</div>
</template>
<script>
import api from '../api/users';
 
export default {
data() {
return {
message: null,
loaded: false,
saving: false,
user: {
id: null,
name: "",
email: ""
}
};
},
methods: {
onSubmit(event) {
this.saving = true;
 
api.update(this.user.id, {
name: this.user.name,
email: this.user.email,
}).then((response) => {
this.message = 'User updated';
setTimeout(() => this.message = null, 10000);
this.user = response.data.data;
}).catch(error => {
console.log(error)
}).then(_ => this.saving = false);
}
},
created() {
api.find(this.$route.params.id).then((response) => {
setTimeout(() => {
this.loaded = true;
this.user = response.data.data;
}, 5000);
});
}
};
</script>
<style lang="scss" scoped>
$red: lighten(red, 30%);
$darkRed: darken($red, 50%);
.form-group label {
display: block;
}
.alert {
background: $red;
color: $darkRed;
padding: 1rem;
margin-bottom: 1rem;
width: 50%;
border: 1px solid $darkRed;
border-radius: 5px;
}
</style>

Homework

After the user update succeeds, we just reset the message after two seconds. Change the behavior to set the message and then redirect the user back to the previous location (i.e., the /users index page).

Second, add a “Back” or “Cancel” button to the bottom of the form that discards the for updates and navigates back to the previous page.

If you are feeling adventurous, display validation errors when the UsersEdit component sends an invalid request to the API. Clear the error messages after successfully submitting the form.

What’s Next

With updating users out of the way, we will move our attention to deleting users. Deleting a user will be helpful to demonstrate programmatically navigating after successful deletion. We will also look at defining a global 404 page now that we have dynamic routing for editing users.

If you’re ready, move on to Part 5.

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
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 →
API Platform for Laravel image

API Platform for Laravel

Read article
Pan - A simple, lightweight, and privacy-focused product analytics php package image

Pan - A simple, lightweight, and privacy-focused product analytics php package

Read article
The Inertia.js v2 Beta is Here image

The Inertia.js v2 Beta is Here

Read article
Mastering Laravel, No Compromises, and SourceDive with Joel Clermont image

Mastering Laravel, No Compromises, and SourceDive with Joel Clermont

Read article
Now you can install PHP and the Laravel installer with a single command image

Now you can install PHP and the Laravel installer with a single command

Read article
WireSpy is a Sleek New Debug Bar for Laravel Livewire image

WireSpy is a Sleek New Debug Bar for Laravel Livewire

Read article