Would you like to buy me a ☕️ instead?
I’m currently in the process of refreshing my knowledge about Laravel. I do so by reading the official documentation. Although, I almost exclusively work on the front side of things with Vue.js nowadays, there is a lot to be learned by getting to know techniques outside of your comfort zone. Laravel is doing some pretty great things to make the life of developers easier. One of those featuers is Route Model Binding.
Route Model Binding in Laravel
This technique makes it possible to achieve a lot with very little code.
Route::get('user/{user}', function (App\User $user) {
return view('user', compact('user'));
});
In this example, a User
model instance is automatically resolved by the given ID
provided in the URL and the data is directly passed into a view
render function. A lot of functionality for three lines of code.
Route Model Binding in Vue.js
Unfortunately, there is no such functionality in Vue.js out of the box. But luckily we’re programmers, and we can write some code to get similar functionality in combination with Vue.js and vue-router
. In the following examples, I assume that you have advanced knowledge about Vue.js and I’ll only show the code which is necessary for this special technique to work. If you want to have a look at the full code, you can go to the GitHub repository accompanying this article.
The store module
In our example implementation, we want to fetch users by ID from an API, we want to use Vuex to manage the state of our application and we want to build our store with namespaced modules.
// src/store/user.js
import axios from 'axios';
export default {
namespaced: true,
actions: {
async byId({ commit }, id) {
const { data: user } = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);
commit('set', user);
},
},
mutations: {
clear(state) {
state.id = null;
state.email = '';
state.username = '';
},
set(state, user) {
state.id = user.id;
state.email = user.email;
state.username = user.username;
},
},
state: {
id: null,
email: '',
username: '',
},
};
In the example above, you can see an action with the name byId()
which we can use to fetch user data from an API. The clear()
and set()
mutations are responsible for mutating the state to either clear it or fill it with new data.
Now that we’ve created our new user store module, we have to load it when initializing our global Vuex store.
// src/store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
import user from './user';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
user,
},
});
The Base model
We want to build our system to be flexible and reusable, therefore let’s start with a Base
model class, which we can later use to extend our data models with it.
// src/models/Base.js
import store from '../store';
export default class Base {
constructor() {
this.fetching = false;
this.idIsInt = true;
this.idKey = 'id';
this.name = this.constructor.name.toLowerCase();
this.store = store;
}
find(rawId) {
const state = this.store.state[this.name];
const id = this.idIsInt ? parseInt(rawId, 10) : rawId;
// If the model is currently fetching data
// we escape the function early and return
// the current `state`.
if (this.fetching) return state;
// If the currently loaded record matches
// the given ID, we can skip fetching data
// and immediately return the `state`.
if (state[this.idKey] !== id) {
this.fetching = true;
// To make sure to not display old data
// the `state` of the store module is cleared
// before filling it with new data.
store.commit(`${this.name}/clear`);
store.dispatch(`${this.name}/byId`, id).then(() => {
this.fetching = false;
});
}
return state;
}
}
Let’s take a look at the code above: in the constructor of our Base
model class, we have the default configuration for our models. We can define if the ID which is passed to our model, will be an integer (idIsInt
) and which key in the state holds the ID (idKey
). The name
of the model, is automatically generated from the constructor.name
(basically the class name) of the model.
The User model
With the Base
model we’ve created in the previous step, we’re now ready to define our User
model.
// src/models/User.js
import Base from './Base';
export class User extends Base {}
export function userModelFactory() {
return new User();
}
Because all of our logic is already defined in the Base
model, our User
model is very tidy and doesn’t has to implement any logic itself.
We’re using a factory function to create a new instance of the User
model. It is generally a good practice to use factory functions if you’re using classes (JavaScript Factory Functions vs Constructor Functions vs Classes - by Eric Elliott).
The User component
In our example, we want to display the name and the email address of a user on a page. Let’s create the component (src/views/User.vue
) which we’ll use to render that page.
<template>
<div class="User">
<p>Username: {{ user.username }}</p>
<p>Email: {{ user.email }}</p>
</div>
</template>
<script>
export default {
name: 'User',
props: {
user: {
type: Object,
},
},
};
</script>
Binding the User model to a route
Now that we’ve laid the foundation, we can start to put everything together and bind our User
model to a new route to render user data in the User.vue
component.
// src/utils/bind-model.js
export default function bindModel(model) {
return route => ({ [model.name]: model.find(route.params[model.name]) });
}
The bindModel()
utility function you can see above, takes a model instance and calls its find()
method with the value of the route parameter matching the value of the models name property. It returns an object with a key which matches the models name.
// src/router.js
import Vue from 'vue';
import Router from 'vue-router';
import User from './views/User.vue';
import { userModelFactory } from './models/User';
import bindModel from './utils/bind-model';
const userModel = userModelFactory();
Vue.use(Router);
export default new Router({
mode: 'history',
routes: [
{
path: '/user/:user',
name: 'user',
component: User,
props: bindModel(userModel),
},
],
});
Now here is where everything comes together. In the route named user
you can see that we pass the object returned by the bindModel()
function, to the props
of the User
component. This makes it possible to access the user
object in the User.vue
component.
That’s it. Thanks to the generic Base
model and the bindModel()
helper function, you can now create new models and store modules without too much effort and without writing a lot of (reapeating) code. And you also don’t have to write the logic for retrieving the data from the store in your components again and again.
Do you want to learn more about advanced Vue.js techniques?
Register for the Newsletter of my upcoming book: Advanced Vue.js Application Architecture.
Bonus: Field mapping with vuex-map-fields
We’ve already achieved our main goal of having a more convenient and abstracted way of automatically loading data by binding a model to a route. As a little bonus, let’s build an additional page where the user can input their data.
Adding vuex-map-fields
vuex-map-fields makes it possible to use two-way data binding (with v-model
) for form fields saved in a Vuex store. In order to being able to use it, we have to update our user store module and the Base
model.
// src/store/user.js
// ...
import { getField, updateField } from 'vuex-map-fields';
export default {
// ...
getters: {
getField,
},
mutations: {
// ...
updateField,
},
// ...
};
In the user store module, we add the vuex-map-fields
getField()
and updateField()
helper functions which enable retrieving and setting data without mutating the state directly.
// src/models/Base.js
import { createHelpers } from 'vuex-map-fields';
import store from '../store';
export default class Base {
// ...
mapFields(fields) {
const { mapFields } = createHelpers({
getterType: `${this.name}/getField`,
mutationType: `${this.name}/updateField`,
});
return mapFields(fields || Object.keys(this.store.state[this.name]));
}
}
In the Base
model, we add a new mapFields()
method which (optionally) takes an array (or object) of fields to be mapped via the mapFields()
function provided by vuex-map-fields
. By default, all of the fields in the store are mapped by their name.
The user create page component
Now we need the view component which we’ll use to render the form fields which the user can use to input their data.
<template>
<form class="UserForm">
<div>
<label>
Username
<input v-model="username">
</label>
</div>
<div>
<label>
Email
<input v-model="email">
</label>
</div>
</form>
</template>
<script>
import { userModelFactory } from '../models/User';
const user = userModelFactory();
export default {
name: 'User',
computed: {
...user.mapFields(),
},
};
</script>
By using the spread operator (...
) we can use the mapFields()
method on the user
model, to map all fields of the user store module to our component. By using v-model
on the input fields, we create a two-way data binding which updates the state via the updateField()
mutation function which is provided by vuex-map-fields
.
Wrapping it up
It can be very inspiring to work with technologies other than those which you’re using day in and day out. Even if you’re not going to use them in your projects, there might be lessons to be learned which also apply to the technologies you’re using.
Laravels Route Model Binding can be a very useful technique in Vue.js applications too and there are tons of other useful things we can learn by looking at other frameworks and programming languages.