DEV Community

John Au-Yeung
John Au-Yeung

Posted on

Add a Progress Bar to Your Vue.js App with Vue-ProgressBar

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. 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/. 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:

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

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

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:

<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 calledJokeByCategory.vue in the views folder and add:

<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:

<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:

<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:

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:

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:

<!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.

Top comments (0)