I've tried to build a fairly simple, reusable and functional Angular2 autocomplete component.

The component:

import { Component, Input, Output, OnInit, ContentChild, EventEmitter, HostListener } from '@angular/core';
import { Observable } from "rxjs/Observable";
import { AutoCompleteRefDirective } from "./autocomplete.directive";

    selector: 'autocomplete',
    templateUrl: './autocomplete.component.html',
    styleUrls: ['./autocomplete.component.css']
export class AutoCompleteComponent implements OnInit {

    public input: AutoCompleteRefDirective;

    @Input() data: (searchTerm: string) => Observable<any[]>;
    @Input() dataMapping: (obj: any) => string;
    @Output() onChange = new EventEmitter<any>();

    @HostListener('document:click', ['$event'])
    clickedOutside($event: any): void {

    public results: any[];
    public query: string;
    public selectedIndex: number = 0;
    private searchCounter: number = 0;

    ngOnInit(): void {
            .subscribe((query: string) => {
                this.query = query;
                let counter = this.searchCounter;

                if (query) {
                        .subscribe(data => {
                            if (counter == this.searchCounter) {
                                this.results = data;
                                this.input.hasResults = data.length > 0;
                                this.selectedIndex = 0;
                else this.clearResults();

            .subscribe(() => {

            .subscribe(() => {
                if (this.results && this.results.length > 0)

            .subscribe(() => {
                if (this.results && this.selectedIndex > 0) this.selectedIndex--;

            .subscribe(() => {
                if (this.results && this.selectedIndex + 1 < this.results.length) this.selectedIndex++;

    selectResult(result: any): void {

    clickedInside($event: any): void {

    private clearResults(): void {
        this.results = [];
        this.selectedIndex = 0;
        this.searchCounter = 0;
        this.input.hasResults = false;

The component HTML:


<div class="autocomplete-wrapper" (click)="clickedInside($event)">
    <div class="list-group autocomplete" *ngIf="results">
        <a [routerLink]="" class="list-group-item" (click)="selectResult(result)" *ngFor="let result of results; let i = index" [innerHTML]="dataMapping(result) | highlight: query" [ngClass]="{'active': i == selectedIndex}"></a>

The component CSS:

.autocomplete-wrapper {
    position: relative;

.autocomplete {
    position: absolute;
    z-index: 100;
    width: 100%;

The directive:

import { Directive, Input, Output, HostListener, EventEmitter } from '@angular/core';

    selector: '[autocompleteRef]'
export class AutoCompleteRefDirective {
    @Input() hasResults: boolean = false;
    @Output() change = new EventEmitter<string>();
    @Output() cancel = new EventEmitter();
    @Output() select = new EventEmitter();
    @Output() up = new EventEmitter();
    @Output() down = new EventEmitter();

    @HostListener('input', ['$event'])
    oninput(event: any) {

    @HostListener('keydown', ['$event'])
    onkeydown(event: any)
        switch (event.keyCode) {
            case 27:
                return false;
            case 13:
                var hasResults = this.hasResults;
                return !hasResults;
            case 38:
                return false;
            case 40:
                return false;

The highlight pipe:

import { Pipe, PipeTransform } from '@angular/core';

    name: 'highlight'

export class HighlightPipe implements PipeTransform {
    transform(value: string, args: any): any {
        var re = new RegExp(args, 'gi');

        return value.replace(re, function (match) {
            return "<strong>" + match + "</strong>";


The implementation:

import { Component } from '@angular/core';
import { Observable } from "rxjs/Observable";
import { Subscriber } from "rxjs/Subscriber";

    selector: 'home',
    template: `
<autocomplete [data]="getData" [dataMapping]="dataMapping" (onChange)="change($event)">
    <input type="text" class="form-control" name="AutoComplete" placeholder="Search..." autocomplete="off" autocompleteRef />
export class HomeComponent {

    getData = (query: string) => this.search(query);

    // The dataMapping property controls the mapping of an object returned via getData.
    // to a string that can be displayed to the use as an option to select.
    dataMapping = (obj: any) => obj;

    // This function is called any time a change is made in the autocomplete.
    // When the text is changed manually, no object is passed.
    // When a selection is made the object is passed.
    change(obj: any): void {
        if (obj) {
            // You can do pretty much anything here as the entire object is passed if it's been selected.
            // Navigate to another page, update a model etc.

    private searchData = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'];

    // This function mimics an Observable http service call.
    // In reality it's probably calling your API, but today it's looking at mock static data.
    private search(query: string): Observable<any>
        return new Observable<any>((subscriber: Subscriber<any>) => subscriber
            .map(o => this.searchData.filter(d => d.indexOf(query) > -1));

A few thoughts:

  • The use of both a Component and a Directive isn't ideal but I couldn't figure out a better way to do it.
  • The getData property and search() functions have been separated so as to mimic a real life service call scenario. It took me an age working out how to wire this up using a property and =>! But is there a better way, especially when dealing with async calls?
  • The highlight pipe isn't necessary but it does improve the experience so I figured I'd include it here.
  • I'm leveraging the bootstrap list group in the component, so apologies if you don't have this installed and it looks crap.
  • I had considered combining the keydown events in the directive into a single event, however because I need to return a value from there I decided to keep them separate.

I'm fairly new to Angular2 so would of course appreciate your suggestions and feedback on any aspect of this implementation.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.