How to Add AutoComplete Input to Your Vue.js App
Tags: vue, webdev, javascript, tutorial
Published: 2020-01-18 03:28:08

**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/](https://exchangeratesapi.io/) to get our exchange rates and the Open Exchange Rates API, located at [http://openexchangerates.org](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:

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

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

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

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

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

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

```javascript
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.