Tutorial: Creating an Angular2 Autocomplete

Angular 2 Autocomplete
Angular 2 Autocomplete

Before we begin I just want to say that I’m not going to use any third-party component for this tutorial, I’m going to create an autocomplete from scratch, my goal is to just make it show the suggestions and allow the user to select one, but if you need more advanced features feel free to comment, I’ll do what I can to help you.

With that in mind, let’s start by creating our class, just copy/paste the code bellow:

export class AutocompleteComponent {
    public query = '';
    public countries = [ "Albania","Andorra","Armenia","Austria","Azerbaijan","Belarus",
                        "Belgium","Bosnia & Herzegovina","Bulgaria","Croatia","Cyprus",
                        "Czech Republic","Denmark","Estonia","Finland","France","Georgia",
                        "Germany","Greece","Hungary","Iceland","Ireland","Italy","Kosovo",
                        "Latvia","Liechtenstein","Lithuania","Luxembourg","Macedonia","Malta",
                        "Moldova","Monaco","Montenegro","Netherlands","Norway","Poland",
                        "Portugal","Romania","Russia","San Marino","Serbia","Slovakia","Slovenia",
                        "Spain","Sweden","Switzerland","Turkey","Ukraine","United Kingdom","Vatican City"];
    public filteredList = [];
    public elementRef;
 
    constructor(myElement: ElementRef) {
        this.elementRef = myElement;
    }
}

As you can see I’ve already added the data to my class, as I’m trying to make it simple it’s just an array of countries, but of course in a real case scenario the data would come from your database or via web service, also it wouldn’t be placed inside the component, it would be passed to it, this way the component could be reused with other data in other parts of the application.

I’ve also created the variables query and filteredList, the first one will hold the string typed by the user, and the second is where I’m going to store the suggestions being displayed by the component, its value will constantly change as the user types on the input. There is also the constructor where I’m setting the elementRef value, but I’ll get to that variable in a moment.

Now let’s create the methods to filter the data and select the item:

filter() {
    if (this.query !== ""){
        this.filteredList = this.countries.filter(function(el){
            return el.toLowerCase().indexOf(this.query.toLowerCase()) > -1;
        }.bind(this));
    }else{
        this.filteredList = [];
    }
}
 
select(item){
    this.query = item;
    this.filteredList = [];
}

These two functions are very straightforward, the filter() function uses the query variable to filter the countries, then it stores the result in the filteredList.

The select() is even simpler, I’m just assigning the selected item to the query variable in order to make it appear on the input, and to make the suggestions list disappear I’m removing everything from the filteredList.

Ok, we already have the two most important functions to make our component work, now we can create the template:

@Component({
    selector: 'autocomplete',
    template: `
        <div class="container" >
            <div class="input-field col s12">
              <input id="country" type="text" class="validate filter-input" [(ngModel)]=query (keyup)=filter()>
              <label for="country">Country</label>
            </div>
            <div class="suggestions" *ngIf="filteredList.length > 0">
                <ul *ngFor="#item of filteredList" >
                    <li >
                        <a (click)="select(item)">{{item}}</a>
                    </li>
                </ul>
            </div>
        </div>  	
        `
})

Note that I’ve used some different css classes here, some of them I created myself and some are from materialize, if you want to use them just add to your project the link to their css and js files.

At this point your autocomplete is already working, if you run your project you’ll see a result similar to the gif at the beginning of this tutorial, but there’s still a small problem we have to fix, note that once the suggestions list shows up you have to either erase the text from the input or select one to make the them disappear. The problem is that the user should be able to get rid of the suggestions by clicking anywhere other than the list, and to be able to do that we have to detect clicks outside the our component, more info here.

Remember the elementRef variable we saw in the constructor? Now we’re going to use it, the follwoing method tells whether the click occured in the component or outside it:

handleClick(event){
   var clickedComponent = event.target;
   var inside = false;
   do {
       if (clickedComponent === this.elementRef.nativeElement) {
           inside = true;
       }
      clickedComponent = clickedComponent.parentNode;
   } while (clickedComponent);
    if(!inside){
        this.filteredList = [];
    }
}

The function just gets the clicked component, navigates between its parent nodes and checks if one of them is the elementRef.nativeElement (which is our component), if it’s true the items from the filteredList are deleted in order to make the list disappear. Now we have to bind this method to the (document:click) event, this is how you can do it:

@Component({
    selector: 'autocomplete',
    host: {
        '(document:click)': 'handleClick($event)',
    },
    template: //TEMPLATE CODE HERE
})

That’s it!! As I said if you need more features or if you have any doubts just leave a comment, I’ll be glad to help!

57 comments on “Tutorial: Creating an Angular2 Autocomplete

  1. Anon

    That’s awesome! I was starting doing my own autocomplete and this will help. Don’t you have a github to upload it so people can contribute? :)

      1. Apurva

        Hi,I want to make an autocomplete in which if i type the city automatically its state should come above it…like cityname and just above it like a superset will be state…
        thanks

  2. Zakkis

    Nice and simple, taught me a few things, thanks for that!
    Actually the autocomplete didn’t work properly for me.

    To make it work I replaced this

    el.toLowerCase().indexOf(this.query.toLowerCase()) > -1;

    with this:

    (el.toLowerCase().substr(0,this.query.length) === this.query.toLowerCase()) == true;
  3. Caaltu

    Thank you for taking your time explaining the functionality as well. Multiselect functionality would be very helpful if you can upgrade it. Thanks

    1. Daniel

      Hi, I have implemented this myself. It isn’t too difficult. First, change template so that the *ngFor looks like this:

      *ngFor="let item of filteredList; let idx = index"

      then your li should have the attribute [class.complete-selected]="idx == selectedIdx" (you can change the class name if you want).

      I also handle the blur event on the input with a handle blur method which looks like this:

      handleBlur(){
              if(this.selectedIdx > -1){
                  this.query = this.filteredList[this.selectedIdx];
              }
              this.filteredList = [];
              this.selectedIdx = -1;
      }

      finally I added the following code after the bind to the filteredList in the filter method (within the if query != "" block of code)

      if (event.code == "ArrowDown" && this.selectedIdx  0) {
           this.selectedIdx--;
      }

      Possibly could be better but it seems to work for now.

      1. Cesar

        Hi Daniel,

        thanks for the info! Please excuse my limited understanding as I’m still ramping up on Angular2. I wasn’t able to get this working and have some questions:

        1) How is the handleBlur() {...} function called?

        I’m assuming you meant adding it to the input field as shown here: Gist of changes made to app.component.ts

        2) Regarding the code placed after the bind to the filterList..

        I’m passing the event in the input field to the filter() function as as shown here:
        Gist of changes made to app.component.ts

        The code seems to execute without errors but I don’t see the focus moving out of the input field to the items nor any item getting selected when clicking on the downArrow key.

        Not sure what I’m missing here. Any help would be highly appreciated!

        1. Daniel Casserly

          Hi Cesar,

          I have put in a pull request to the github that has the full code changes. I would use the (blur)="handleBlur" as this will be called on all blur rather than KeyUp.

          You would need to have a class in your css that mimics the :hover for the li elements. (In my personal project this includes making the background orange). Also, my original comment had the ArrowUp code too (you’ll find it in the github pull request).

          The git hub pull request is here: https://github.com/leonardohjines/angular2-autocomplete/pull/1

          Daniel

        2. P

          @Cesar, some comments which will hopefully help you.

          1. the handleClick is being configured from the host section of the component so I can only presume that you’d have to include something similar there for handleBlur:

          host: {
             '(document:click)': 'handleClick($event)',
          },

          2. make sure you add styling for the complete-selected class to your css

  4. sridevips

    Hi, Code is awesome. But i Face the Following Error in keyup, when I tried this code.

      ORIGINAL EXCEPTION: TypeError: el.toLowerCase is not a function

    I don’t know how to solve this problem. Plz help me.

    1. Curtis

      TypeError: “el.toLowerCase is not a function” means that the typeof “el” doesn’t have a “.toLowerCase” function.

      The type of “el” should be “String”.

      Example:

            / /Update the filteredList
            this.filteredList = this.dataArray.filter(function (el: String) {
              return el.toLowerCase().indexOf(this.query.toLowerCase()) &gt; -1;
            }.bind(this));
  5. David N

    Thank’s for this – you’ve really helped with a custom autocomplete… Question? How about arrow keys support (i.e. up/down & enter to select)? Any suggestions?

  6. kruti

    how do i use it inside different component where i can add multiple drop-downs for different values?
    lets say a user sign up form with country , state as two different dropdowns

  7. Stacy

    Hi,

    I am new in Angular2 and I used your Git project for the autocomplete component. After “npm – start ” I get the following errors:

    app/shared/autocomplete/autocomplete.component.ts(19,5): error TS7008: Member 'filteredList' implicitly has an 'any[]' type.
    app/shared/autocomplete/autocomplete.component.ts(20,5): error TS7008: Member 'elementRef' implicitly has an 'any' type.
    app/shared/autocomplete/autocomplete.component.ts(30,65): error TS7006: Parameter 'el' implicitly has an 'any' type.
    app/shared/autocomplete/autocomplete.component.ts(43,12): error TS7006: Parameter 'item' implicitly has an 'any' type.
    app/shared/autocomplete/autocomplete.component.ts(57,17): error TS7006: Parameter 'event' implicitly has an 'any' type.
    app/contact/autocomplete.component.ts(1,37): error TS2307: Cannot find module 'angular2/core'.
    app/contact/autocomplete.component.ts(33,5): error TS7008: Member 'filteredList' implicitly has an 'any[]' type.
    app/contact/autocomplete.component.ts(34,5): error TS7008: Member 'elementRef' implicitly has an 'any' type.
    app/contact/autocomplete.component.ts(44,65): error TS7006: Parameter 'el' implicitly has an 'any' type.
    app/contact/autocomplete.component.ts(57,12): error TS7006: Parameter 'item' implicitly has an 'any' type.
    app/contact/autocomplete.component.ts(71,17): error TS7006: Parameter 'event' implicitly has an 'any' type.

    Can you please help me?
    Thank You!

    1. Curtis

      Specify the type for public variables and each parameter of select(), handleClick(), and the first parameters function parameters for Array.prototype.filter().

      // String typed by user
        public query: String = '';
        // TODO - Turn into service
        public dataArray: Array = ['Albania', 'Andorra', 'Armenia', 'Austria', 'Azerbaijan', 'Belarus',
          'Belgium', 'Bosnia &amp; Herzegovina', 'Bulgaria', 'Croatia', 'Cyprus',
          'Czech Republic', 'Denmark', 'Estonia', 'Finland', 'France', 'Georgia',
          'Germany', 'Greece', 'Hungary', 'Iceland', 'Ireland', 'Italy', 'Kosovo',
          'Latvia', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Macedonia', 'Malta',
          'Moldova', 'Monaco', 'Montenegro', 'Netherlands', 'Norway', 'Poland',
          'Portugal', 'Romania', 'Russia', 'San Marino', 'Serbia', 'Slovakia', 'Slovenia',
          'Spain', 'Sweden', 'Switzerland', 'Turkey', 'Ukraine', 'United Kingdom', 'Vatican City'];
        // Suggestions
        public filteredList: Array = [];
        // Element reference to directive
        public elementRef: ElementRef = null;
       
        select(item: String) {}
        ...
        handleClick(event: Event) {}
        ...
        this.countries.filter(function (el: String) {...}.bind(this));
        ...
  8. thandar win

    This is working .But this doesn’t show example image. I want to show example image.
    so please……How do ????

    i write frmAutoComplete.component.ts.
    ===================================

    import { Router, RouteParams, CanDeactivate, ComponentInstruction } from 'angular2/router';
    import {Component, Input, OnDestroy} from 'angular2/core';
    import {Subscription}   from 'rxjs/Subscription';
    import {RpIntercomService} from '../framework/rp-intercom.service';
    import {RpInputComponent} from '../framework/rp-input.component';
    import {RpHttpService} from '../framework/rp-http.service';
    import {RpBean} from '../framework/rp-bean';
    import {ElementRef} from 'angular2/core';
    declare var jQuery: any;
    // Application Specific
    import {ClientUtil} from '../util/rp-client.util';
    @Component({
        selector: 'autocomplete',
        host: {
            '(document:click)': 'handleClick($event)',
        },
        template: //TEMPLATE CODE HERE
         `
     
     
     
                  Country
     
                 0>
     
     
                            <a>{{item}}</a>
     
     
     
     
            `
     
    })
    export class AutocompleteComponent {
        public query = '';
        public countries = [ "Albania","Andorra","Armenia","Austria","Azerbaijan","Belarus",
                            "Belgium","Bosnia &amp; Herzegovina","Bulgaria","Croatia","Cyprus",
                            "Czech Republic","Denmark","Estonia","Finland","France","Georgia",
                            "Germany","Greece","Hungary","Iceland","Ireland","Italy","Kosovo",
                            "Latvia","Liechtenstein","Lithuania","Luxembourg","Macedonia","Malta",
                            "Moldova","Monaco","Montenegro","Netherlands","Norway","Poland",
                            "Portugal","Romania","Russia","San Marino","Serbia","Slovakia","Slovenia",
                            "Spain","Sweden","Switzerland","Turkey","Ukraine","United Kingdom","Vatican City"];
        public filteredList = [];
        public elementRef;
     
        constructor(myElement: ElementRef) {
            this.elementRef = myElement;
        }
     
        filter() {
        if (this.query !== ""){
            this.filteredList = this.countries.filter(function(el){
                return (el.toLowerCase().substr(0,this.query.length) === this.query.toLowerCase()) == true;
            }.bind(this));
        }else{
            this.filteredList = [];
        }
        console.log("Filtered list"+this.filteredList );
    }
     
    select(item){
        this.query = item;
        this.filteredList = [];
    }
    handleClick(event){
       var clickedComponent = event.target;
       var inside = false;
       do {
           if (clickedComponent === this.elementRef.nativeElement) {
               inside = true;
           }
          clickedComponent = clickedComponent.parentNode;
       } while (clickedComponent);
        if(!inside){
            this.filteredList = [];
        }
    }
    }

    ==================================================
    I use this at frmDailyLog.component.ts.
    ====================================================

    import {AutocompleteComponent} from '../ui/frmAutoComplete.component';
    @Component({
      selector: 'frmDailyLogComponent',
      template: ` 
     
     
     
       ` ,
      directives: [RpInputComponent,RpInputSimpleComponent,AutocompleteComponent],
      providers: [RpHttpService]
    })............

    =========================================================
    please,help me and tell me how to do and change my code?

      1. aseni m.

        is it possible to make a generic autocomplete component, and pass the country list items to the generic component? and how about making it cascading for state/city selection.

  9. Lakshmi

    Hi, thanks for this tutorial. It saved me a lot of trouble. I have one question with regard to your code on plunker. In the list of dropdown elements, I want it such that when I use the arrow buttons, I don’t want hover to work and vice versa. At all points either hover or drop down should work . I should be able to switch from hovering with mouse to using the keyboard dropdown and vice verse. I am not sure how to implement this functionality. Thanks!!

  10. Stacy

    I have problem when selecting a list item with the cursor, the input field is not showing the targeted item, but only when some arrow key is pressed first. Is there maybe something else that I need to change as well?

    Thanks!

    1. bea

      You have to cancel the (blur) event, both the html attribute and the class method.
      So delete (blur)=handleBlur() from tag and the handleBlur() method from AppComponent class.

  11. Mac Mold

    I changed its style, code for accepting json objects … and many more things but all in all its an component code given – I liked it alot ..

  12. Mi

    I tried to change the output of the list by changing and by replacing them with and . However when I select an element in my list, it doesn’t fill the input anymore… Any clues on how to do this ?

  13. Satar

    Hi, Leonardo.
    I have trouble:
    – the select(item) function not reacted when clicked on array list.
    why it is happened?
    Can not find issue about one week, thanks

  14. Hoon

    Hi Leonardo,

    I’m also running in the trouble of the select(item) function. When I hover over the suggestion and click with my mouse, the ‘query’ does not seem to get updated to fill up the selected country on the input field.

  15. Swetha

    can you help with loading dropdownlist with favourite list of items and user can use from them or search and select from them.

  16. Nils

    I tried to use the ArrowDown (and ArrowUp) functionality described by Daniel:

                if (event.code == "ArrowDown" &amp;&amp; this.selectedIdx  0) {
                    this.selectedIdx--;
                }

    However, I get an exception “Cannot read property ‘code’ of undefined” when typing in the input field. What have I missed?

  17. Vishal

    Have you thought about using templates? I mean I want to bind country object instead of just string i.e. name of the country. For e.g. I want to show country name as well as its flag. Can you give me a start for such option?

  18. Toufik

    Srinivas Guddati
    Yes, you can make it work on ionic2 without having to change input to ion-input, I am working on ionic2 and it worked for me like charm 😀

  19. Yasir

    Hi, the auto-complete works fine… but how do we add a Bold prefix on the shown list… I have seen another example (ionic2-autocomplete) and they are using a pipe for that, but i couldn’t implement it here.

    here is the pipe:

          ///
          @Pipe({
          name: 'boldprefix'
         })
          @Injectable()
            export class BoldPrefix implements PipeTransform {
                transform(value: string, keyword: string): any {
              return value.replace(new RegExp(keyword, 'gi'), function(str) { return str.bold(); });
            }
        }

    How do we use this in this example

  20. CJay

    Nice but up and down arrow key is not working.

    Any help,how to add Keyboard Event select list item using up and down arrow key with scrollbar.

    Thanks in Advance.

  21. Manu

    Hi,
    Thks for the tutorial it’s really great. I am neebie in angular 2 and I would to know if it’s possible and hard to improve your code ta pass an array of object instead of an array of string ?

  22. Mariya Baby

    Hello,

    I want a functionality where I don’t want force selection. The functionality to be implemented is. On input box click, I want a list of options to come up and select from the options if I find the required one. Or else, i want to type in the input box and click save.

  23. Abelardo

    Hi,
    I am integrating this component into my Angular 5 app.

    When I run it, this message appears:
    core.js:1350 ERROR Error: Uncaught (in promise): Error: StaticInjectorError[ElementRef]:

    I imported it my .ts component: should I import it inside my app.module.ts again?

    Best regards.

Leave a Reply to Daniel Casserly Cancel reply

Your email address will not be published. Required fields are marked *

Obs: Use the tag <pre lang="LANGUAGE"> to include code blocks to your comment.
Example: <pre lang="javascript"> console.log('Test'); </pre>