Heads up: this article is over a year old. Some information might be out of date, as I don't always update older articles.

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.

comments powered by Disqus

Manage content in a ProcessWire website using Telegram

Introduction

In this tutorial we will learn how to manage content in a ProcessWire website using a Telegram bot.

Today …