In this tutorial we are going to build simple VueJS 2 Typeahead Component.We won’t use any libraries such as Axios, jQuery or VueResource - instead we are going to use Fetch API, that’s built into the browser.Usage of Fetch API is pretty straightforward, so bear with me. For project scaffholding we are going to use Vue CLI with standard Webpack template option.I assume you have Vue CLI already installed on your machine, so let’s start.

Creating Vue 2 Project

First thing first, open your working directory in terminal and then type this command

$ vue init webpack vue-typeahead-component

You will be prompted to enter a project name, description, author and Vue build.We won't install Vue Router.Things such as ESLint, Unit Testing and End to End Testing are optional, so feel free to install them If you want to. Once we have initialized our app, we will need to install the required dependencies.You can use npm or yarn, depending on what you have installed

# cd into the directory
$ cd vue-typeahead-component
# install dependencies with npm or yarn
$ npm install
# or
$ yarn install

After successful dependencies installation, go and start development server with following command

$ npm run dev

This command will start development server with Hot Reloading and other cool stuff in your default Browser.

Project Organization

In this tutorial we will follow the standard directory and files structure that Vue CLI offers, we will mostly work in src directory so our src directory should looks like this

├── App.vue
├── components
│   └── Typeahead.vue
└── main.js

NOTE: As you can see I’ve removed the assets directory, and components/Hello.vue because we don’t need them.This is not required step, but I suggest you to make It, to keep your project sturcture clean as much It’s possible.

Go and create Typeahead.vue component and place It into the components directory.

Building Component

Our Single File Vue Component - Typeahead.vue would have the 3 sections - template, script(logic) and style. so let’s quickly build that stuff.

<template>
    <div>

    </div>
</template>

<script>
    export default {
        name: 'Typeahead'
    }
</script>

<style>
    .SearchInput {
        width: 100%;
        padding: 1.5em 1em;
        font-size: 1em;
        outline: 0;
        border: 5px solid #41B883;
    }
    .Results {
        margin: 0;
        padding: 0;
        text-align: left;
        position: relative;
    }
    .Results li {
        background: rgba(53, 73, 94, 0.3);
        margin: 0;
        padding: 1em;
        list-style: none;
        width: 100%;
        border-bottom: 1px solid #394E62;
        transition: ease-in-out 0.5s;
    }
    .fade-enter-active, .fade-leave-active {
        transition: opacity 0.3s;
    }
    .fade-enter, .fade-leave-to {
        opacity: 0;
    }
</style>

NOTE: This tutorial doesn't put focuss on CSS Learning, so I'm not going to explain anything related to CSS here.

When I write component, what I like first think about is the props that my component would accept and their validation.When we are talking about props validation in VueJS 2, It's just a joke and pretty easy process.First you have to define props object and then all props related to this component should be placed under that props object

...
<script>
    export default {

       name: 'Typeahead',

       props: {
            source: {
                type: [String, Array],
                required: true
            },
            filterKey: {
                type: String,
                required: true
            },
            startAt: {
                type: Number,
                default: 3
            },
            placeholder: {
                type: String,
                default: ''
            }
       }    

    }
</script>
...

Now when we have our props defined, let’s briefly explain them

source is prop that could be passed down as Array or String.The reason why we allowed passing 2 diferent types is because user could pass API Endpoint (URL) which is String, or he could pass already builded array of objects from parent component.That array could be already fetched from another source, or simply hardcoded into the app.This prop is required, so in case nothing is passed down, VueJS would throw warning into the console.

filterKey is prop that could be passed only as String.The purpose of this prop is filtering by specific property in iterated object.Imagine that your API returns an array of objects, where each object has title property, and when user type something in input box, It would filter things by title.

startAt is prop that could be passed only as Number and It has default value 3.This prop would determine when the filtering process will start.By default It would start with filtering when user type 3 characters.

placeholder is prop type that could be passed only as String and It default value is empty string ''.It's just text that would be applied to text input type as placeholder.

After defining props that my component could accept, I like to continue with structuring application model/state in VueJS known as data.So far I know that we are going to have two properties defined into the our model aka data object.

items which is empty array with purpose of keeping the data passed down via source prop

query which is empty string and It would be binded to v-model on input text type.Since value of query would be updated instantly, while user typing (thanks to VueJS two-way binding) we can use that for real-time filtering without page reload.

<script>
    export default {
        data() {
            return {
                items: [],
                query: ''
            }
        }
    }
</script>

NOTE: data into the Vue Component must be a function that return object.In standard constructed Vue Instance It should be object.

Since we have our model defined, we could go and fill the items array with data that comes via source prop, so let’s define our method called the fetchItems.Each method in VueJS goes into the methods object.

<script>
    export default {
        methods: {
            fetchItems() {
                if ( typeof this.source === 'string' ) {
                    fetch(this.source)
                        .then(stream => stream.json())
                        .then(data => this.items = data)
                        .catch(error => console.error(error))
                } else {
                    this.items = this.source
                }
            }
        }
    }
</script>

As you can see we have small conditional checking here with typeof operator.Remember that our prop source could be Array or String ? When user pass the String down, he passed the API Endpoint so if typeof source is String we are going to use Fetch API and store response to the items array in our model, however if user pass array manually, we are just going to assign that passed array to items array.

fetch method is related to Fetch API, and as you can see It accept the endpoint URL and return promise.In first then block it would return something that we could call blob, and here we set the type of our data, as we know that we are dealing with JSON API, we will return it as json(). Second then block refers to the actual data, so we are just assigning data from response to the items array in component model.And also if there is any error we will log It into the console with catch block - in case promise is not resolved.

NOTE: I’m using ES6 Arrow Syntax here and this keyword is binded to correct (parent) context.In case you are use standard functions, this context would be binded to fetch object itself, so you will have to bind it explicitly to correct context (which should be Vue Instance/Component), otherwise you’ll got error that items is not defined.

Now, when we have our fetchItems method created, let’s call it into the mounted hook so we have data when component is mounted

<script>
    export default {
        mounted() {
            this.fetchItems()
        }
    }
</script>

Now It's time to quickly jump into the template section of our Vue Component.For now we will just create the text input and bind v-model to query property.

<template>
    <div>
        <input 
            v-model="query"
            type="text" 
            class="SearchInput" 
            :placeholder="placeholder"
        >        
    </div>
</template>

Cool, we have our data stored into the items array and text input that’s binded to query property from model, now we can dive into the making filtering logic.For this type of task, we are going to use computed properties which are, IMO one of the best feature that VueJS offers.Computed properties are reactive, they change their value only when state changes and they must return something.

Let's name our computed property filtered - you can name It as you wish.First thing that we are going check is when we want to start filtering things out.Remember that we have prop called startAt so we are going to filter things out when query length is greater/equal to the startAt value that’s passed down.

<script>
    export default {
        computed: {
            filtered() {
                if(this.query.length >= this.startAt) {
                    // ...
                }
            }
        }
    }
</script>

Now when we have that checking done, we can move to actual filtering logic.For filtering we are going to use filter method that lives on Array.prototype, basically filter is method that returns new array and accept the callback that determines what type of filtering would be done.

<script>
    export default {
        computed: {
            filtered() {
                if(this.query.length >= this.startAt) {
                    return this.items.filter(item => {
                        // ...
                    })
                }
            }
    }
</script>

Before we return the filtered item(s), we want to check does the property passed by filterKey exist on iterated object and then we will return something only if that key exist, in case It doesn’t we will throw console error.We are going to use hasOwnProperty method.

<script>
    export default {
        computed: {
            filtered() {
                if(this.query.length >= this.startAt) {
                    return this.items.filter(item => {
                        if( item.hasOwnProperty(this.filterKey)  ) {
                            // ...
                        } else {
                            console.error(`Seems like property you passed down ${this.filterKey} doesn't exist on object ! `)
                        }
                    })
                }
            }
        }
    }
</script>

And finally let’s filter out the actual items that matches the typed query.We are going to use indexOf method that lives on String prototype.

MDN Reference: The indexOf() method returns the index within the calling String object of the first occurrence of the specified value, starting the search at fromIndex. In case the typed value is not found, It would return the -1, so we can use that for comparation, so If value is greater that -1, then we have something found.

<script>
        computed: {
            filtered() {
                if(this.query.length >= this.startAt) {
                    return this.items.filter(item => {
                        if( item.hasOwnProperty(this.filterKey)  ) {
                            return item[this.filterKey]
                                .toLowerCase()
                                .indexOf(this.query.toLowerCase()) > -1
                        } else {
                            console.error(`Seems like property you passed down ${this.filterKey} doesn't exist on object ! `)
                        }
                    })
                }
            }
        }
</script>

As you can see we are making everything lowercase because things are case sensitive.In case of you have property that content starts with the capital letter, and user typed lowercase even that this value exist It won’t find It and -1 would be returned, so to prevent that the easiest thing is make everything lowercase, including the query property.

Cool, now we have our core logic done, let’s go back into the template again and render out things that would be filtered.As you could see in Demo here is a simple animation when found items(s) appear/dissapear bellow the input textbox.To make that working with unordered lists in conjuction with v-for we have to use transition-group component that’s built in the Vue.

<template>
    <div>
        <input 
            v-model="query"
            type="text" 
            class="SearchInput" 
            :placeholder="placeholder"
        >
        <transition-group name="fade" tag="ul" class="Results">
            <li v-for="item in filtered" :key="item">
                <span>
                    <strong>{{ item.title  }}</strong> - <small>{{ item.id  }}</small><br>
                    <small>{{ item.body  }}</small>
                </span>
            </li>
        </transition-group>
    </div>
</template>

As you can see lis are nested into the transition-group component, tag specified for transition group is ul with class .Results and transition name fade as well, in DOM It would be rendered as normal ul with lis as childs and specified class.

What If nothing is found for typed term ? It would be cool If we display message to user when there are no found matches.Well with VueJS is pretty easy, let’s write another computed property called isEmpty that will return the Boolean.

<script>
    export default {
        computed: {
            isEmpty() {
                if( typeof this.filtered === 'undefined'  ) {
                    return false
                } else {
                    return this.filtered.length < 1
                }
            }
        }
    }
</script>

NOTE: By default filtered will return undefined, so we have to return false for default state to prevent showing up message when nothing is not typed into the input box.In else statement we have condition that checks if array length is less than 1, and If condition is matched then It would return true, which means that there are no matches for specific input.

Now when we have the computed property that checks for the found result, let’s integrate that into our template.We will use v-show directive on paragraph that we will place just below our transition-group

<template>
    <div>
        <input 
            v-model="query"
            type="text" 
            class="SearchInput" 
            :placeholder="placeholder"
        >
        <transition-group name="fade" tag="ul" class="Results">
            <li v-for="item in filtered" :key="item">
                <span>
                    <strong>{{ item.title  }}</strong> - <small>{{ item.id  }}</small><br>
                    <small>{{ item.body  }}</small>
                </span>
            </li>
        </transition-group>
        <p v-show="isEmpty">Sorry, but we can't find any match for given term :( </p>
    </div>
</template>

As the final touch It would be cool if we have reset method that would clear the input, so let’s create it.

<script>
    export default {
        methods: {
            reset() {
                this.query = ''
            }
        }
    }
</script>

I'd like attach blur event on input element, and fire reset method on blur event.

<template>
    <div>
        <input 
            v-model="query"
            @blur="reset"
            type="text" 
            class="SearchInput" 
            :placeholder="placeholder"
        >
        <transition-group name="fade" tag="ul" class="Results">
            <li v-for="item in filtered" :key="item">
                <span>
                    <strong>{{ item.title  }}</strong> - <small>{{ item.id  }}</small><br>
                    <small>{{ item.body  }}</small>
                </span>
            </li>
        </transition-group>
        <p v-show="isEmpty">Sorry, but we can't find any match for given term :( </p>
    </div>
</template>

Final Touches

At the end let's integrate our component into the application and try to use It.Open App.vue that's generated by Vue CLI, and make It looks like this

<template>
    <div id="app">
         <div class="Wrap text-center">
             <h1>Vue Typeahead</h1>
             <p>Simple VueJS 2 TypeAhead component builded with Fetch Browser API.</p>

             <!-- Our component-->
             <typeahead
                source="https://jsonplaceholder.typicode.com/posts"
                placeholder="What TV Serie you are looking for..."
                filter-key="title"
                :start-at="2"
             >
             </typeahead>   
         </div>
    </div>
</template>

<script>

    import Typeahead from './components/Typeahead.vue'

    export default {

        name: 'app',

        components: { Typeahead  }

    }

</script>

<style>
    body {
        margin: 0; padding: 0;
        font-family: 'Open Sans', Arial, sans-serif;
        box-sizing: border-box;
        background: linear-gradient(135deg, #41B883 0%,#354953 100%);
        background-size: cover;
        min-height: 100vh;
        color: #fff;
    }
    h1 {
        font-size: 6vw;
    }
    h1, h2, h3, h4 {
        margin: 0; padding: 0;
        font-family: 'Lobster', Arial, sans-serif;
    }
    .text-center {
        text-align: center;
    }
    .Wrap {
        max-width: 60vw;
        margin: 15vh auto;
    }
</style>

NOTE: This part is not related to our component directly - It's just small check that everything works as suposed.

Conclusion

In this tutorial we have learned how to create simple, but useful type ahead component without any external library for making HTTP Requests such as Axios, jQuery or VueResource.We also learned about some core concepts in VueJS such as props validation, computed properties creation, organizing your Vue Project, and we quickly go over the Fetch API as well. I hope you learned something from this tutorial, try to add something else on your own - It’s easy.Cheers !

Like this article? Follow @bedakb on Twitter