Introduction
At work we are building a complex Single Page Application with VueJS, VueX and VueRouter. As soon as it started to get bigger we found the need to add breadcrumbs to the main interface, in order to indicate to the user exactly where he/she is and to provide an effective navigation mechanism.
At first we searched for existing solutions and libraries, however we only found bits of inspiration, but nothing really ready to use, so we decided to roll out our own solution for the purpose.
The first try
The first solution we tried to implement was static, leveraging the meta
field of each route. So for example, having the following routes:
var routes = [
{
// parent route record
path: '/users',
name: 'users-index',
meta: {
text: 'Users'
},
children: [
{
// child route record
path: 'create',
name: 'users-new',
meta: {
text: 'Create User'
}
},
// ...
]
}
]
we used the $route.matched
property to get the route records for all nested path segments of the current route, in parent to child order. So for example when the URL is /users/create
we can iterate over $route.matched
and extract the meta.text
property as follows:
<ul>
<li v-for="route in $route.matched">
<router-link :to="{name: route.name}">
{{ route.meta.text }}
</router-link>
</li>
</ul>
Unfortunately this approach gets really tricky when we have to deal with dynamic data. Let’s suppose that we add a route for showing an existing user that matches the following URL /users/1
:
var routes = [
{
// parent route record
path: '/users',
name: 'users-index',
meta: {
text: 'Users'
},
children: [
// ...
{
path: ':user',
name: 'users-show',
meta: {
text: '???'
}
},
// ...
]
}
]
What are we going to display here? It’s obvious that Users | Show User
doesn’t make a lot of sense, neither it does showing the ID of the user Users | 1
. Wouldn’t instead to be nice to display the dynamic data fetched from the backend Users | John Doe
?
The VueX approach
We finally decided to leverage VueX as a global source of truth, letting each “page” component to manage its own breadcrumbs. For this purpose we decided to build a dedicated VueX module with very simple push/pop/empty logic. The result was the following:
export default {
namespaced: true,
state: {
// array of breadcrumb objects, in the form of VueRouter
// descriptors (see https://router.vuejs.org/api/#to)
breadcrumbs: [],
},
mutations: {
set(state, breadcrumbs) {
state.breadcrumbs = breadcrumbs;
},
push(state, breadcrumb) {
state.breadcrumbs.push(breadcrumb)
},
pop(state) {
state.breadcrumbs.pop();
},
replace(state, payload) {
const index = state.breadcrumbs.findIndex((breadcrumb) => {
return breadcrumb.text === payload.find;
});
if (index) {
state.breadcrumbs.splice(index, 1, payload.replace);
}
},
empty(state) {
state.breadcrumbs = [];
}
}
}
At first we were tempted to build a VueJS plugin in order to inject this functionality at a global-level, however it was clear that only “page” component should have been responsible for breadcrumbs. Therefore we decided to explicitly inject the functionality only to those specific components. For this purpose we implmented a little mixin for mapping VueX mutations to local components’ actions.
import { mapMutations } from 'vuex';
export default {
methods: {
...mapMutations('breadcrumbs', {
setBreadcrumbs: 'set',
pushBreadcrumb: 'push',
popBreadcrumb: 'pop',
replaceBreadcrumb: 'replace',
emptyBreadcrumbs: 'empty'
})
}
}
and finally we imported the mixin in each page component. Notice that when the created
hook gets called, we only set static breadcrumbs, using a placeholder (:user
) indicating where dynamic data should be injected. As soon as the data is fetched from the backend we replace the placeholder with the actual text:
// URL: /users/1
import BreadcrumbsManager from 'mixins/BreadcrumbManager';
export default {
name: 'UsersShow',
mixins: [BreadcrumbsManager],
created () {
this.setBreadcrumbs([
{ text: 'Home', to: { path: '/' }},
{ text: 'Users', to: { name: 'users-index'}},
{ text: ':user' } // placeholder
]);
axios.get(`/api/users/${this.$route.params.user}`)
.then(response => response.data)
.then(user => {
this.user = user;
this.replaceBreadcrumb({
find: ':user',
replace: { text: user.name }
});
});
}
}
Conclusions
That’s it for this post! If you manage your breadcrumbs in a different way please let me know in the comments.