typeahead.js directive for angular.js

Some fellows at Twitter open-sourced their implementation of autocomplete.  They called it typeahead.js. Head over to their documentation pages to read more details about this awesome library. It is important that you understand the parameters/options that the library needs in order to configure it, and make it work.

In this post, I was tinkering on making an angularJS directive to wrap the typeahead.js autocomplete library.  The end product, would look like something as shown below. Note that I am using Bootstrap 3 CSS library to style my web page.

image

Some dependencies in this sample project:

  • Using jQuery
  • Using Bootstrap 3
  • Using underscore.js – to ease array processing in this sample project
  • Using the “controller as” syntax

Also, the assumption is that the api call providing the suggestions returns an array of “complex” objects as shown below

image

HTML markup

<input type="text" class="form-control" name="inputCity" placeholder="Your City"
    required data-ng-model="vm.city" data-typeahead="City" data-url-remote="/api/QTest/SuggCity"
    data-url-prefetch='/api/QTest/SomeCities' data-ta-template='<p><strong>%%City%%</strong>,&nbsp;%%Province%%</p>' 
/>

I called my directive typeahead, hence the attribute data-typeahead=”City” . “City” in this case is the property of the chosen datum from the array of datums that I will eventually pass to the controller, hence data-ng-model=vm.city (plain string). The rest of the data-* attributes are used to pass some configuration stuffs for the typeahead.js library.

The controller

(function () {
 'use strict';

var controllerId = 'typeController';
 angular.module('app').controller(controllerId,
 ['$scope', typeController]);

function typeController($scope) {
 var vm = this;
 vm.City = '';
 }
})();

Nothing special really…

And now, the directive

(function () {
    'use strict';
    /** simple template engine that adheres to https://github.com/twitter/typeahead.js
    * Hogan.js was recommended, but to me, overkill for this simple purpose!
    */
    var ENGINE = {
        cache: {},
        generate: function (str, data) {
            // based on https://gist.github.com/padolsey/6008842
            str = str.replace(/%% *([\w_]+) *%%/g, function (str, key) {
                return '" + o["' + key + '"]' + (typeof data[key] === 'function' ? '(o)' : '') + ' + "';
            });
            // jshint evil: true
            // render function is required
            return { render: new Function('o', 'return "' + str + '";') };
        },
        compile: function (str, data) {
            data = data || {};




            var t = this.cache[str];
            if (t) {
                return t;
            }
            t = this.generate(str, data);
            return this.cache[str] = t;
        }
    };




    var directiveId = 'typeahead';
    app.directive(directiveId, ['$parse', function ($parse) {




        // Usage:
        // 
        // Creates:
        // 
        var directive = {
            link: link,
            restrict: 'A',
            require: '?ngModel'
        };
        return directive;
















        function link(scope, element, attrs, ctrl) {
            if (!ctrl) return; // do nothing if no ng-model
            var getter = $parse(attrs.ngModel),
                setter = getter.assign;




            element.on('$destroy', function () {
                try {
                    $(element).typeahead('destroy');
                } catch (e) {




                }




            });




            var opts = {
                name: attrs[directiveId], // local cache name!
                valueKey: attrs[directiveId], // points to City in this example
                engine: ENGINE // was Hogan.js, overkill!
            };




            if (attrs.urlPrefetch) {
                opts.prefetch = { url: attrs.urlPrefetch, filter: transform };
            }




            if (attrs.urlRemote) {
                opts.remote = { url:  attrs.urlRemote + '?q=%QUERY', filter: transform };
            }








            if (attrs.taTemplate) {
                opts.template = attrs.taTemplate;
            }




            $(element).typeahead(opts);




            $(element).bind('typeahead:selected', function (obj, datum) {
                $(element).blur();
                scope.$apply(function () {




                    setter(scope, datum[attrs[directiveId]]);




                });
            });




            /**
            * Sample transform function implementation
            * Uses underscore.js
            */
            function transform(parsedResponse) {
                return _.map(parsedResponse, function (item) {
                    if (!item.tokens) {
                        var inputs = _.values(item),
                        results = [];
                        _.each(inputs, function (inp) {
                            results = _.union(results, tokenize(inp));
                        });
                        item.tokens = results;
                    }
                    return item;
                });
            }
            /**
            * Split sentences into tokens so each word
            * is searcheable as well
            */
            function tokenize(str) {
                return $.trim(str).toLowerCase().split(/[\s\-_]+/);
            }
        }




    }]);
})();

Some clarifications:

When using templates to display the autocomplete choices, typeahead.js requires that you use a templating engine. In their documentation, they suggest using Hogan.js. In this sample project, I thought it was overkill to use this templating engine since my autocomplete suggestions display is quite simple.

The transform function (used by the filter option property) shouldn’t be necessary in this implementation if the api result payload already contains tokens. I added it here, as a scenario wherein we wanted lesser bytes sent over the wire… The transform function creates tokens from every string data of the datum – to help in creating hints for typeahead.js. This is useful especially if you only use either prefetch (cached) or local mode as the remote mode will always issue an api call. It is then up to your api method on how you compute for the suggestions based on the query string sent. Here are some sample api methods (ASP .Net Web API implementation).

[HttpGet]
public List<CityModel> SomeCities()
{




    var results = new List<CityModel>();
    results.Add(new CityModel()
    {
        City = "Timbuktu",
        NationalAnthem = Ipsum.GetPhrase(10),
        Country = "Canada",
        Population = 30000000,
        Province = "Alberta"
    });




    return results;
}




[HttpGet]
public async Task<List<CityModel>> SuggCity(string q)
{
    return await Task.Factory.StartNew<List<CityModel>>((p) =>
    {
        var hint = (string)p;
        var results = new List<CityModel>();
        results.Add(new CityModel()
        {
            City = "Cayley",
            NationalAnthem = Ipsum.GetPhrase(10),
            Country = "Canada",
            Population = 100,
            Province = "Alberta"
        });




        results.Add(new CityModel()
        {
            City = "Canmore",
            NationalAnthem = Ipsum.GetPhrase(10),
            Country = "Canada",
            Population = 5000,
            Province = "Alberta"
        });
        results.Add(new CityModel()
        {
            City = "Sunnyvale",
            NationalAnthem = Ipsum.GetPhrase(10),
            Country = "USA",
            Population = 100000,
            Province = "California"
        });




        //filter blah blah
        return results.Where(_ => _.City.ToUpper().Contains(hint.ToUpper())).ToList();




    }, state: q);
}
Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer’s view in any way.
Tagged with: , , ,
Posted in angularJS
  • arief nur andono

    plunkr example would make easier to view code.. :)