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.