Create a searchable dropdown list in Vue
How to build a dynamically filtered dropdown list in Vue.js
Before we start, let’s take a look at what we’re building. The aim is to create a Vue.js component to display an input field, that upon input, will display a dropdown list of items, dynamically filtered to match the input value. We’ll be retrieving the list data from a RESTful API.
Prerequisites
This article is suitable for Vue.js beginners and above. It is assumed that you understand simple Javascript and the basic outline of Vue Single-File Components (.vue files).
Aims
- Retrieve the data from an API
- Display a list of items when input is provided
- Filter out items that do not match the input
- Allow the user the select an item
Retrieving the data
Firstly, we need to retrieve our data. In my example I’ll be using Axios, to retrieve data from the Rest Countries free RESTful API.
Why Axios?
Axios is a simple promise based HTTP client that allows us to easily create XMLHttpRequests/AJAX requests without worrying about browser support.
Installing Axios
Install Axios by running the following terminal command in your project directory:
$ npm install axios
Initial script
We begin by creating a DropdownList.vue file, importing Axios, and creating two variables.
itemList
will contain our dataapiLoaded
is initialised tofalse
, and will be updated totrue
after our data is loadedapiUrl
contains the link to the API, which in my example will behttps://restcountries.eu/rest/v2/all?fields=name;flag
Create DropdownList.vue:
<template></template><script>
import axios from 'axios'export default {
data () {
return {
itemList: [],
apiLoaded: false,
apiUrl: 'https://restcountries.eu/rest/v2/all?fields=name;flag'
}
}
}
</script>
Using Axios
Now we’ll retrieve and store the API data. Create a method, getList
, that fetches the data, passes it to itemList
, and updates apiLoaded
to true. We run getList
when the component is mounted
.
mounted () {
this.getList()
},
methods: {
getList () {
axios.get(this.apiUrl).then( response => {
this.itemList = response.data
this.apiLoaded = true
})
}
}
Perfect, now we’ve got the data we can start creating the component.
Creating the base template
Next we’ll create the basic HTML template for the DropdownList.vue
component, as well as creating a second component for each list item.
Create the template
Create a template with a containing element .dropdown
, a text input field .dropdown-input
, and a container to hold the list .dropdown-list
, as follows. You can enter placeholder text as a label for the input field.
<template>
<div class="dropdown">
<input class="dropdown-input" type="text" placeholder="Find country" />
<div class="dropdown-list"></div>
</div>
</template>
Getting the search term
Add a new local property to data, inputValue
, and initialise it as an empty string. We’ll be using this to hold the text field input.
data () {
return {
inputValue: '',
...
}
}
Now, to bind the input text to inputValue
we’ll be using v-model
. The v-model
Vue directive links the value of an input to a data ()
property; if one changes, then so does the other. Add the following to .dropdown-input
’s properties:
<input v-model.trim="inputValue" ... />
Whenever .dropdown-input
’s value changes, inputValue
will be passed its value, and vice versa. Removing whitespace either side of your input is often a good idea, and this is why we’re using the .trim
modifier here.
Building the logic
Now we’ll write the list and search logic:
- Create the list of items
- If the user is searching, we’ll check if each item contains the search term
- And if the item does not contain the search term, we’ll hide it
Generating the list
We’ll use v-for
to generate the list of items, passing the data to each .dropdown-item
, in my case a country’s name and flag. Place the following markup inside of the .dropdown-list
element:
<div v-for="item in itemList" :key="item.name" class="dropdown-item">
<img :src="item.flag class="dropdown-item-flag"/>
{{ item.name }}
</div>
Here, v-for
is looping through itemList
, containing our API data, assigning each item to item
. We’re assigning a unique value to :key,
because this is helpful for Vue to keep track of the different elements created using v-for
.
As you can see, our API data is in this format, therefore item.name
provides a country name, and item.flag
provides the URL of a flag image:
[
{"flag":"https://restcountries.eu/data/alb.svg","name":"Albania"},
{"flag":"https://restcountries.eu/data/dza.svg","name":"Algeria"},
...
]
Filter the list
Next we’ll create a method to check if each item should be visible or not. We’ll compare the current user input with the data of the item, and return true
or false
depending on whether it partially, or fully, matches.
methods: {
itemVisible (item) {
let currentName = item.name.toLowerCase()
let currentInput = this.inputValue.toLowerCase()
return currentName.includes(currentInput)
},
...
}
Here, includes()
returns true
if currentInput
can be found within currentName
, and false
if not. We also converted both the country name and the user input to lower case, so that the user doesn’t need to perfectly match the letter case of the country name when searching.
Pass the method to the items, and hide list
Now we pass the new method to .dropdown-item
with v-show
like so:
<div v-show="itemVisible(item)" ... >
This will apply display: none
to .dropdown-item
if itemVisible()
returns true
for the current item
. We should now have a working (unstyled) filter, try it out!
Hide the list unless searching
Adding the following to .dropdown-list
will hide the list unless the API is loaded, and the user has started searching:
<div v-show="inputValue && apiLoaded" ... >
inputValue
will return false
if it contains an empty string ''
, and true
otherwise, essentially checking for any user input. Then we check to see if the data is loaded, and if both are true
the element is visible.
Using v-show instead of v-if
Using v-show
(which hides the item) is preferable to using v-if
(which removes the item from the DOM completely) in this situation, because repeatedly manipulating the DOM can be less performant than toggling display: none
.
Styling
We don’t want the dropdown to interfere with the page layout, we’d like it to float on top, so we apply the following styles:
.dropdown{
position: relative;
}.dropdown-list{
position: absolute;
}
You should now be able to search for items in the list!
I’ve applied a number of other non-essential CSS rules to style my component in the Gist below, I’d recommend copying the <style>
section if you’re creating my country name list.
Selecting and resetting the item
Now we need to write a method to select, and display, the chosen item, and another method to reset it.
Selecting an item
First, create a new data ()
property, selectedItem
, to hold the selected item:
data (){
return {
selectedItem: {},
...
}
}
And now, we’ll create a simple method to pass an item to the new property, and then clear the text input (don’t worry, we’ll explain the last line later):
method: {
selectItem (theItem) {
this.selectedItem = theItem
this.inputValue = ''
this.$emit('on-item-selected', theItem)
}
...
}
And we’ll pass this method to .dropdown-item
with a @click
handler, passing on theitem
from the v-for
loop. In other words, when the element is clicked, the new method will be run. Add to .dropdown-item
:
<div @click="selectItem(item)" ... >
Hiding the input
The input should only be visible when a selected item has not been chosen, and should be replaced by an element displaying the chosen item afterwards.
Add the following to .dropdown-input
:
<input v-if="Object.keys(selectedItem).length === 0" ... />
This is the easiest way to check if an object is empty or not. It returns a list of keys held by selectedItem
, in this case, if an item is selected, ['name', 'flag']
and then we check the length
. If length === 0
then the object has no keys, is empty, and we have not selected an item.
Displaying the selected item
Now we can display the new element. Add the following directly after .dropdown-input
:
<div v-else @click="resetItem" class="dropdown-selected">
<img :src="selectedItem.flag" class="dropdown-item-flag" />
{{ selectedItem.name }}
</div>
An element with v-else
directly after one with v-if
will only display itself if the v-if
element does not. So .dropdown-selected
will only appear if .dropdown-input
(which is directly before and contains a v-if
attribute) does not.
Inside of .dropdown-selected
we’ve copied the code from inside of .dropdown-item
and swapped out for item
for selectedItem
. We’ll create theresetItem
medthod in the next step.
Resetting an item
We’ll use .dropdown-selected
as a button to reset the selection. When clicking on it, we want to reset the current item, display the input, and allow another choice to be made. After displaying the input again, we would like to focus
back on the input element, so that the user can type without clicking.
This is quite simple to do. In the last step we provided a @click
handler to the .dropdown-selected
element, now we’ll write that method:
methods: {
resetItem () {
this.selectedItem = {}
this.$nextTick( () => this.$refs.dropdowninput.focus() )
this.$emit('on-item-reset')
},
...
}
Now add a ref
attribute to .dropdown-input
. This is a simple way to create an identifier for an element, that Vue can manipulate. Avoiding symbols and capitals makes for easier access here!
<input ref="dropdowninput" ... />
Explanation
So what did we just do with this method? The first line of resetItem ()
resets the selected item to an empty object. The second line, in particular this.$refs.dropdowninput.focus()
, simply finds the input element referenced by ref
and focusses on it.
$nextTick
is used because the method is run before the DOM has updated, and the .dropdown-input
has not been drawn yet. $nextTick
waits until the next page draw, and then runs the code block contained within it.
You should now have a working, filterable, and selectable, dropdown list!
Communicating the selection to a parent
The final step is to add this component to your Vue app, and pass on the selection information to the instance’s parent.
Earlier, we added the following to selectItem ()
and resetItem ()
respectively:
this.$emit('on-item-selected', theItem) this.$emit('on-item-reset')
Those two statements trigger custom events that we can catch within the <DropdownList>
tag in a parent component, to retrieve the selected item, and then reset it. Here’s an example of a .vue
file importing our dropdown list component, and catching our events:
Now we can access the selected item data from within dropdownSelection
, perfect!
Conclusion
There you have it! A reusable, filterable, dropdown list component in Vue.js.
I hope this tutorial has been helpful, thanks for reading!