Typeahead and Aurelia

The pace of my blogging has dropped somewhat now I’m on a full-time engagement, and so far there haven’t been too many stories to share from the engagement. But the one that was perhaps most frustrating and in need of sharing was getting Twitter typeahead working with Aurelia.
I will prefix this with a disclaimer – these are steps as I best recollect them and I haven’t re-done them all as I’m not in the mood for breaking something that is (finally) working.

No longer maintained?

The first obstacle was finding the source code for typeahead. The sourced linked from the official looking page (linked earlier) points to this GitHub repository which, at the time of writing, had no commits for almost two years. However various google searches intimated more recent changes, and eventually it became clear that most people were referring to this which, in its own words, “is a maintained fork of twitter.com’s autocomplete search library, typeahead.js.”

Getting the library

The working typeahead repository can be obtained through npm:
npm install corejs-typeahead --save

This does not come with an index.d.ts so if you’re like me and using TypeScript then you’ll need a typings file. Despite the strong possibility it is for the wrong typeahead repository, the typings file returned by typings search typeahead seemed to work fine once installed by typings install dt~typeahead -GS

Typeahead is dependent on jQuery v2, so after fetching via npm, in my package.json I fixed the version as follows "jquery": "^2". Finding a typings for jQuery was straightforward but getting TypeScript to be happy with it was harder. In the end I found simply import 'jquery'; to be the most effective way of including it in a TypeScript file. Then there was the challenge with a collision on the $ variable (with angular-protractor) which I could only resolve by editing the typings file for jQuery:

declare module 'jquery' {
    export = jQuery;
}
declare var jQuery: JQueryStatic;
//declare var $: JQueryStatic;

This does mean jQuery must be referenced as jQuery(...) rather than $(...) in source code, but personally I don’t mind – as good as jQuery is, my sensitivities find there to be something a tiny bit wrong with a library deciding it can own a single letter global variable.

Aurelia Bundles

Typeahead isn’t really a module and thus the TypeScript declaration file doesn’t export one, but it does for Bloodhound which is hard to avoid if you want to do anything useful with typeahead. To import these they have to be included in the aurelia bundle, and best as I can tell, they have to be separate. So my aurelia.json looks like this:

"jquery",{
    "name": "Bloodhound",
    "path": "../node_modules/corejs-typeahead",
    "main": "dist/bloodhound",
    "deps": ["jquery"]
},{
    "name": "typeahead",
    "path": "../node_modules/corejs-typeahead",
    "main": "dist/typeahead.jquery",
    "deps": ["jquery", "Bloodhound"]
},

To actually use them, they can be fully imported as follows:

import 'jquery';
import 'Bloodhound';
import 'typeahead';

Bootstrap Integration

At this stage typeahead was working, but didn’t fit in with the UI which was otherwise styled via Bootstrap 3. The advice from the internet was to use Bootstrap-3-Typeahead which seemed fine initially, but I simply couldn’t get working with remote data sources. After a lot of stepping through library source code and periodic googling I discovered this bridge simply didn’t support remote (according to that ticket it may now have been fixed).

So instead of using the bridge, I got the styles from here and applied a couple of modifications:

.tt-hint {
    border: 0
}
/* Support inline use */
.form-inline span.twitter-typeahead {
  width: auto;
  vertical-align: middle;
}

The Code

The end result is an aurelia element that does typeahead. The code for that is in a gist here

The element requires a prepare-query function which sets the remote url and applies any necessary headers. This is an example of one:

prepareQueryFn(query, settings: JQueryAjaxSettings) {
    settings.url = this.apiUrl + 'search?query=' + encodeURIComponent(query);
    settings.headers = { 
        'Accept': 'application/json',
        'Authorization': this.getAuthToken()
    }
    return settings;
}