Add a Progress Bar to Your Vue.js App with Vue-ProgressBar
Tags: vue, tutorial, beginners, webdev
Published: 2020-02-07 02:16:47

**Subscribe to my email list now at http://jauyeung.net/subscribe/**

**Follow me on Twitter at https://twitter.com/AuMayeung**

**Many more articles at https://medium.com/@hohanga**

**Even more articles at http://thewebdev.info/**

A progress bar is a line that shows how close to completion something is in a GUI app. It provides a good user experience for users because they can know when something is complete and how close to completion it is, making users’ minds more comfortable.

Vue.js has many progress bar libraries built for it. One of them is Vue-ProgressBar, located at [https://github.com/hilongjw/vue-progressbar](https://github.com/hilongjw/vue-progressbar). It is easy to incorporate to any Vue.js app and it’s very flexible, with lots of options you can change.

In this article, we will build an app that displays Chuck Norris jokes from the Chuck Norris Jokes API, located at [https://api.chucknorris.io/](https://api.chucknorris.io/). The app will have a home page for displaying a random joke, a page that lets users look for a random joke by category, and a search page to search for jokes. To start, we will run the Vue CLI by running:

```
npx @vue/cli create chuck-norris-app
```

In the wizard, we select the ‘Manually select features’ and select Vue Router and Babel.

Next, we install some packages. We will use Axios for making HTTP requests, BootstrapVue for styling, Vue-ProgressBar for adding our progress bar, and Vee-Validate for form validation. To install them, we run:

```bash
npm i axios bootstrap-vue vue-progressbar vee-validate
```

Next, we create a`mixins` folder in the `src` folder and create a file called `requestsMixin.js` file. In there, we add:

```javascript
const APIURL = "https://api.chucknorris.io/jokes";
const axios = require("axios");
export const requestsMixin = {
  methods: {
    getRandomJoke() {
      return axios.get(`${APIURL}/random`);
    },
    getJokeByCategory(category) {
      return axios.get(`${APIURL}/random?category=${category}`);
    },
    getCategories() {
      return axios.get(`${APIURL}/categories`);
    },
    searchJokes(query) {
      return axios.get(`${APIURL}/search?query=${query}`);
    }
  }
};
```

This file has the code to call all the endpoints of the Chuck Norris Jokes API to get the jokes and categories, and also search for jokes by keyword.

Next in the `views` folder, we replace the code in the `Home.vue` file with:

```javascript
<template>
  <div class="page">
    <h1 class="text-center">Random Joke</h1>
    <p>{{joke.value}}</p>
  </div>
</template>
<script>
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
  name: "home",
  mixins: [requestsMixin],
  data() {
    return {
      joke: {}
    };
  },
  beforeMount() {
    this.$Progress.start();
    this.getJoke();
  },
  methods: {
    async getJoke() {
      const { data } = await this.getRandomJoke();
      this.joke = data;
      this.$Progress.finish();
    }
  }
};
</script>
```

With the Vue-ProgressBar library, we have the `this.$Progress` object available in all our components since we will add it to `main.js` . We call the `this.$Progress.start();` to display the progress bar right before the HTTP request is made by calling the `this.getRandomJoke` function from `requestsMixin` . Then once the response is successfully retrieved, then we call `this.$Progress.finish();` to make the progress bar disappear. In the template, we display the joke.

Next, create a file called`JokeByCategory.vue` in the `views` folder and add:

```javascript
<template>
  <div class="page">
    <h1 class="text-center">Joke by Category</h1>
    <ValidationObserver ref="observer" v-slot="{ invalid }">
      <b-form novalidate>
        <b-form-group label="Category">
          <ValidationProvider name="category" rules="required" v-slot="{ errors }">
            <b-form-select v-model="category" :options="categories" @change="getJoke()"></b-form-select>
            <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
          </ValidationProvider>
        </b-form-group>
      </b-form>
    </ValidationObserver>
<p>{{joke.value}}</p>
  </div>
</template>
<script>
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
  mixins: [requestsMixin],
  data() {
    return {
      category: "",
      categories: [],
      joke: {}
    };
  },
  beforeMount() {
    this.getJokeCategories();
  },
  methods: {
    async getJokeCategories() {
      this.$Progress.start();
      const { data } = await this.getCategories();
      this.categories = data.map(d => ({
        value: d,
        text: d
      }));
      this.$Progress.finish();
    },
    async getJoke() {
      this.$Progress.start();
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        this.$Progress.finish();
        return;
      }
      const { data } = await this.getJokeByCategory(this.category);
      this.joke = data;
      this.$Progress.finish();
    }
  }
};
</script>
```

In the `beforeMount` hook, we run the `getJokeCategories` , which call `this.getCategories` from the `requestsMixin` to get the categories when the page loads.

This page works the same as `Home.vue` . We display the progress bar when requests are started and remove it when the request is finished. This file makes 2 requests, one to get the categories from the API with the `this.categories` function from the `requestsMixin` and the `this.getJokesByCategory` function from the same file.

In the `getJoke` function, we validate our form with Vee-Validate by calling `this.$refs.observer.validate();` to make sure `category` is selected before getting the joke.

We use Vee-Validate to validate the form fields. The `ValidationObserver` component is for validating the whole form, while the `ValidationProvider` component is for validating the form fields that it wraps around.

The validation rule is specified by the `rule` prop of the `category` field. The `state` prop is for setting the validation state which shows the green when `errors` has length 0 and red otherwise.

The error messages are shown in the `b-form-invalid-feedback` component. This page only has the countries dropdown.

Next, we add a `Search.vue` file in the `views` folder and add:

```javascript
<template>
  <div class="page">
    <h1 class="text-center">Search</h1>
    <ValidationObserver ref="observer" v-slot="{ invalid }">
      <b-form novalidate @submit.prevent="onSubmit">
        <b-form-group label="Keyword">
          <ValidationProvider name="keyword" rules="required" v-slot="{ errors }">
            <b-form-input
              type="text"
              :state="errors.length == 0"
              v-model="keyword"
              required
              placeholder="Search "
              name="keyword"
            ></b-form-input>
            <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
          </ValidationProvider>
        </b-form-group>
<b-button type="submit" variant="primary">Search</b-button>
      </b-form>
    </ValidationObserver>
<p v-for="(j, i) of jokes" :key="i">{{j.value}}</p>
  </div>
</template>
<script>
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
  mixins: [requestsMixin],
  data() {
    return {
      keyword: "",
      jokes: []
    };
  },
  methods: {
    async onSubmit() {
      this.$Progress.start();
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        this.$Progress.finish();
        return;
      }
      const {
        data: { result }
      } = await this.searchJokes(this.keyword);
      this.jokes = result;
      this.$Progress.finish();
    }
  }
};
</script>
```

We let users search for jokes from the API.

In the `onSubmit` function, we validate our form with Vee-Validate by calling `this.$refs.observer.validate();` to make sure `category` is selected before getting the joke. We use Vee-Validate to validate the form fields. The `ValidationObserver` component is for validating the whole form, while the `ValidationProvider` component is for validating the form fields that it wraps around. The validation rule is specified by the `rule` prop of the `category` field. The `state` prop is for setting the validation state which shows the green when `errors` has length 0 and red otherwise. The error messages are shown in the `b-form-invalid-feedback` component. This page only has the countries dropdown.

The progress bar works the same as the other components. We display the progress bar when searching for jokes by calling `this.$Progress.start();` , then `this.searchJokes` and remove it when the request is finished. Finally `this.$Progress.finish();` is called to make the progress bar disappear.

Next in `App.vue` , we replace the existing code with:

```javascript
<template>
  <div id="app">
    <vue-progress-bar></vue-progress-bar>
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand to="/">Chuck Norris Jokes App</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item to="/" :active="path  == '/'">Home</b-nav-item>
          <b-nav-item to="/jokebycategory" :active="path  == '/jokebycategory'">Jokes By Category</b-nav-item>
          <b-nav-item to="/search" :active="path  == '/search'">Search</b-nav-item>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>
    <router-view />
  </div>
</template>
<script>
export default {
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  }
};
</script>
<style lang="scss">
.page {
  padding: 20px;
}
button,
.btn.btn-primary {
  margin-right: 10px !important;
}
.button-toolbar {
  margin-bottom: 10px;
}
</style>
```

to add a Bootstrap navigation bar to the top of our pages, and a `router-view` to display the routes we define.

Next in `main.js` , replace the code with:

```javascript
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";
import VueProgressBar from "vue-progressbar";
extend("required", required);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.use(BootstrapVue);
Vue.use(VueProgressBar, {
  color: "rgb(143, 255, 199)",
  failedColor: "red",
  height: "2px"
});
Vue.config.productionTip = false;
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");
```

so that we add the libraries we installed to our app so we can use it in our components. We call `extend` from Vee-Validate to add the form validation rules that we want to use. Also, we add the Vue-ProgressBar library here so we can use it in all our components. When we include it with `Vue.use` , we pass in the progress bar options as the second argument. In this app, we set the color to a greenish color, failed color to be red, and the height to be 2 pixels. We also imported the Bootstrap CSS in this file to get the styles.

In `router.js` , we replace the existing code with:

```javascript
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import JokeByCategory from './views/JokeByCategory.vue'
import Search from './views/Search.vue'
Vue.use(Router)
export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/jokebycategory',
      name: 'jokebycategory',
      component: JokeByCategory
    },
    {
      path: '/search',
      name: 'search',
      component: Search
    }
  ]
})
```

to include our home and search pages.

Finally, in `index.html` , replace the existing code with:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <title>Chuck Norris Jokes App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-progress-bar-tutorial-app doesn't work properly
        without JavaScript enabled. Please enable it to continue.</strong
      >
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>
```

to change the title.