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
To let users select from long lists easily, an input with autocomplete is preferable to a plain select dropdown because it lets users search for the entry they want instead of selecting from a list. This is a common feature of web apps, so the developer has developed autocomplete components that we add the feature easily.
In this article, we will make a currency converter that lets users select currencies to convert to and list exchange rates by the base currency. We will use Vue.js to build the app, use the Foreign Exchange Rate API located at https://exchangeratesapi.io/ to get our exchange rates and the Open Exchange Rates API, located at http://openexchangerates.org, to get our list of currencies.
To start building the app, we will run the Vue CLI to create the project. Run npx @vue/cli create currency-converter
to create the project. In the wizard, we select ‘Manually select features’ and pick Babel, CSS Preprocessor, and Vuex, and Vue Router from the list.
Next, we install some libraries. We will use Axios for making HTTP requests, BootstrapVue for styling, Vee-Validate for form validation, and Vue-Autosuggest for the autocomplete input. Vue-Autosuggest lets us customize all parts of the component. It does not have any opinion on styling, which means that it fits well with Bootstrap styles.
We install all the packages by running npm i axios bootstrap-vue vee-validate vue-autosuggest
to install all the libraries.
Next, we write the code for our app. We start by adding a mixin for sending our HTTP requests to the APIs to get data. Create a mixins
folder in the src
folder and then add requestsMixin.js
in the src
folder, then add the following code to the file:
const APIURL = "https://api.exchangeratesapi.io";
const OPEN_EXCHANGE_RATES_URL =
"http://openexchangerates.org/api/currencies.json";
const axios = require("axios");
export const requestsMixin = {
methods: {
getCurrenciesList() {
return axios.get(OPEN_EXCHANGE_RATES_URL);
},
getExchangeRates(baseCurrency) {
return axios.get(`${APIURL}/latest?base=${baseCurrency}`);
}
}
};
We’re using Axios to make the requests to the APIs.
Next, we build a page to let users convert currencies. Create ConvertCurrency.vue
in the views
folder and add:
<template>
<div class="page">
<h1 class="text-center">Convert Currency</h1>
<ValidationObserver ref="observer" v-slot="{ invalid }">
<b-form @submit.prevent="onSubmit" novalidate>
<b-form-group label="Amount" label-for="title">
<ValidationProvider name="amount" rules="required|min_value:0" v-slot="{ errors }">
<b-form-input
v-model="form.amount"
type="text"
required
placeholder="Amount"
name="amount"
></b-form-input>
<b-form-invalid-feedback :state="errors.length == 0">Amount is required</b-form-invalid-feedback>
</ValidationProvider>
</b-form-group>
<b-form-group label="Currency to Convert From" label-for="start">
<ValidationProvider name="fromCurrency" rules="required" v-slot="{ errors }">
<vue-autosuggest
:suggestions="filteredFromCurrencies"
:input-props="{id:'autosuggest__input', placeholder:'Select Currency to Convert From', class: 'form-control'}"
v-model="form.fromCurrency"
:get-suggestion-value="getSuggestionValue"
:render-suggestion="renderSuggestion"
component-attr-class-autosuggest-results-container="result"
@selected="onSelectedFromCurrency"
></vue-autosuggest>
<b-form-invalid-feedback
:state="errors.length == 0"
>Currency to Convert From is required</b-form-invalid-feedback>
</ValidationProvider>
</b-form-group>
<b-form-group label="Currency to Convert To" label-for="end">
<ValidationProvider name="toCurrency" rules="required" v-slot="{ errors }">
<vue-autosuggest
:suggestions="filteredToCurrencies"
:input-props="{id:'autosuggest__input', placeholder:'Select Currency to Convert To', class: 'form-control'}"
v-model="form.toCurrency"
:get-suggestion-value="getSuggestionValue"
:render-suggestion="renderSuggestion"
component-attr-class-autosuggest-results-container="result"
@selected="onSelectedToCurrency"
></vue-autosuggest>
<b-form-invalid-feedback :state="errors.length == 0">Currency to Convert To is required</b-form-invalid-feedback>
</ValidationProvider>
</b-form-group>
<b-button type="submit" variant="primary">Convert</b-button>
</b-form>
</ValidationObserver>
<div v-if="convertedAmount" class="text-center">
<h2>Converted Amount</h2>
<p>{{form.amount}} {{selectedFromCurrencyCode}} is equal to {{convertedAmount}} {{selectedToCurrencyCode}}</p>
</div>
</div>
</template>
<script>
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
name: "ConvertCurrency",
mixins: [requestsMixin],
computed: {
currencies() {
return Object.keys(this.$store.state.currencies).map(key => ({
value: key,
name: this.$store.state.currencies[key]
}));
},
filteredFromCurrencies() {
const filtered =
this.currencies.filter(
c =>
(c.value || "").toLowerCase() !=
(this.selectedToCurrencyCode || "").toLowerCase() &&
(c.value || "")
.toLowerCase()
.includes((this.form.fromCurrency || "").toLowerCase())
) ||
(c.name || "")
.toLowerCase()
.includes((this.form.fromCurrency || "").toLowerCase());
return [
{
data: filtered || []
}
];
},
filteredToCurrencies() {
const filtered =
this.currencies.filter(
c =>
(c.value || "").toLowerCase() !=
(this.selectedFromCurrencyCode || "").toLowerCase() &&
(c.value || "")
.toLowerCase()
.includes((this.form.toCurrency || "").toLowerCase())
) ||
(c.name || "")
.toLowerCase()
.includes((this.form.toCurrency || "").toLowerCase());
return [
{
data: filtered || []
}
];
}
},
data() {
return {
form: {
currency: ""
},
exchangeRates: {},
ratesFound: false,
selectedFromCurrencyCode: "",
selectedToCurrencyCode: "",
convertedAmount: 0
};
},
methods: {
getSuggestionValue(suggestion) {
return suggestion && suggestion.item.name;
},
renderSuggestion(suggestion) {
return suggestion && suggestion.item.name;
},
onSelectedFromCurrency(item) {
this.selectedFromCurrencyCode = item && item.item.value;
},
onSelectedToCurrency(item) {
this.selectedToCurrencyCode = item && item.item.value;
},
async onSubmit() {
const isValid = await this.$refs.observer.validate();
if (!isValid) {
return;
}
try {
const { data } = await this.getExchangeRates(
this.selectedFromCurrencyCode
);
const rate = data.rates[this.selectedToCurrencyCode];
this.convertedAmount = this.form.amount * rate;
} catch (error) {}
}
}
};
</script>
The list of currencies are retrieved when App.vue
loads and stored in the Vuex store so we can use it in all our pages without reloading the request for getting the currencies list.
We use Vee-Validate to validate our inputs. We use the ValidationObserver
component to watch for the validity of the form inside the component and ValidationProvider
to check for the validation rule of the inputted value of the input inside the component. Inside the ValidationProvider
, we have our BootstrapVue input for the amount
field.
The Vue-Autosuggest components to let users select the currencies they want to convert from and to. The suggestions
prop contains the list of currencies filtered by what the user inputted and also filters out what the currency that the other field is set to. The input-props
prop contains an object with the placeholder of the inputs. v-model
has sets what the user has entered so far, which we will use in the scripts
section to filter out currencies. get-suggestion-value
prop takes a function that returns the suggested items in a way that you prefer. render-suggestion
prop displays the selection in a way you prefer by passing in a function to the prop. The component-attr-class-autosuggest-results-container
lets us set the class for the result drop-down list and the selected
event handler lets us set the final value that is selected.
In the filteredFromCurrencies
and filteredToCurrencies
functions, we filter out the currencies by excluding the currency already entered the other drop-down and also filter by what the user has entered so far in a case insensitive manner.
Once the user clicks Save, then the onSubmit
function is called. Inside the function, this.$refs.observer.validate();
is called to check for form validation. observer
is the ref of the ValidationObserver
. The observed form validation value is here. If it resolves to true
, We get the exchange rates for the base currency by calling the getExchangeRates
function which is added from the mixin and then convert it to the final converted amount and display it in the template below the form.
Next in Home.vue
, replace the existing code with:
<template>
<div class="page">
<h1 class="text-center">Exchange Rates</h1>
<vue-autosuggest
:suggestions="filteredCurrencies"
:input-props="{id:'autosuggest__input', placeholder:'Select Currency', class: 'form-control'}"
v-model="form.currency"
:get-suggestion-value="getSuggestionValue"
:render-suggestion="renderSuggestion"
component-attr-class-autosuggest-results-container="result"
@selected="onSelected"
>
<div slot-scope="{suggestion}">
<span class="my-suggestion-item">{{suggestion.item.name}}</span>
</div>
</vue-autosuggest>
<h2>Rates</h2>
<b-list-group v-if="ratesFound">
<b-list-group-item v-for="(key, value) in exchangeRates.rates" :key="key">{{key}} - {{value}}</b-list-group-item>
</b-list-group>
<b-list-group v-else>
<b-list-group-item>Rate not found.</b-list-group-item>
</b-list-group>
</div>
</template>
<script>
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
name: "home",
mixins: [requestsMixin],
computed: {
currencies() {
return Object.keys(this.$store.state.currencies).map(key => ({
value: key,
name: this.$store.state.currencies[key]
}));
},
filteredCurrencies() {
const filtered = this.currencies.filter(
c =>
(c.value || "")
.toLowerCase()
.includes(this.form.currency.toLowerCase()) ||
(c.name || "")
.toLowerCase()
.includes(this.form.currency.toLowerCase())
);
return [
{
data: filtered
}
];
}
},
data() {
return {
form: {
currency: ""
},
exchangeRates: {},
ratesFound: false
};
},
methods: {
getSuggestionValue(suggestion) {
return suggestion.item.name;
},
renderSuggestion(suggestion) {
return suggestion.item.name;
},
async onSelected(item) {
try {
const { data } = await this.getExchangeRates(item.item.value);
this.exchangeRates = data;
this.ratesFound = true;
} catch (error) {
this.ratesFound = false;
}
}
}
};
</script>
<style lang="scss" scoped>
</style>
This is the home page of our app. On the top, we have the Vue-Autosuggest component to filter user inputs from the list of currencies. The currencies list is from the Vuex store. Once the user selected their final value, we run this.getExchangeRates
, which is from the requestsMixin
, to load the latest exchange rates for the selected currency if they are found.
Next in App.vue
, replace the existing code with:
<template>
<div id="app">
<b-navbar toggleable="lg" type="dark" variant="info">
<b-navbar-brand to="/">Currency Converter</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="/convertcurrency" :active="path == '/convertcurrency'">Convert Currency</b-nav-item>
</b-navbar-nav>
</b-collapse>
</b-navbar>
<router-view />
</div>
</template>
<style lang="scss">
.page {
padding: 20px;
}
.result {
position: absolute;
background-color: white;
min-width: 350px;
z-index: 1000;
ul {
margin: 0;
padding: 0;
border: 1px solid #ced4da;
border-radius: 3px;
li {
list-style-type: none;
padding-left: 10px;
}
}
}
</style>
<script>
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
mixins: [requestsMixin],
data() {
return {
path: this.$route && this.$route.path
};
},
watch: {
$route(route) {
this.path = route.path;
}
},
beforeMount() {
this.getCurrencies();
},
methods: {
async getCurrencies() {
const { data } = await this.getCurrenciesList();
this.$store.commit("setCurrencies", data);
}
}
};
</script>
Here we add the BootstrapVue navigation bar. We also have the router-view
for showing our routes. In the scripts
section, we watch the $route
variable to get the current route the user has navigated to set the active
prop of the b-nav-item
. Also, when this component loads, we get the currencies and put it in our Vuex store so that we get the data in all of our components. We load it here because this is the entry component for the app.
This component also holds the global styles for our app. The result
class is for styling the autocomplete dropdown. We set position
to absolute
so that it displays above everything else, and allowing it to overlap with other items. We also set the color of the drop-down and added a border to it. The dot for the list items are removed with list-style-type
set to none
. We have the page
class to add some padding to our pages.
Next in main.js
replace the existing 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 VueAutosuggest from "vue-autosuggest";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required, min_value } from "vee-validate/dist/rules";
extend("required", required);
extend("min_value", min_value);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.use(VueAutosuggest);
Vue.use(BootstrapVue);
Vue.config.productionTip = false;
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");
We add BootstrapVue, Vue-Autosuggest, and Vee-Validate to our app here. In addition, we add the Vee-Validate validation rules that we use here, which include the required
rule to make sure everything is filled, and the min_value
for the amount. The Bootstrap CSS is also included here to styling all our components.
Then in router.js
, replace the existing code with:
import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import ConvertCurrency from "./views/ConvertCurrency.vue";
Vue.use(Router);
export default new Router({
mode: "history",
base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: Home
},
{
path: "/convertcurrency",
name: "convertcurrency",
component: ConvertCurrency
}
]
});
to add our routes so users can see our pages.
In store.js
replace the existing code with:
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
currencies: {}
},
mutations: {
setCurrencies(state, payload) {
state.currencies = payload;
}
},
actions: {}
});
to store the list of currencies that we use in all our components. We have the setter function in the mutation
object and the currencies
state which is observed by our components.
Then in index.html
, we 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>Currency Converter</title>
</head>
<body>
<noscript>
<strong
>We're sorry but vue-autocomplete-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.