Building a Typeahead Directive with AngularJS
I am building a little project that lets the user keep a list of artists and albums and I needed a typeahead / autocomplete that would match on such things. I took inspiration from the last.fm typeahead:
I am also using the last.fm api.
My first thought was to wrap the Twitter Bootstrap typeahead in Angular. The problem is I wanted full control over the html in the typeahead. For instance I wanted little sidebars that said ‘artists’ and ‘albums’. I had no idea how to do this with the Bootstrap typeahead. Another problem is from a purely aesthetic point of view I don’t like wrapping things in Angular if the problem itself is easily solved using Angular.
I thought this would be a good project to tackle with Angular so I wrote my own.
The starting point was to create a custom component. Angular lets you define your own html elements and so the
typeahead element was born!
It doesn’t do much yet.
Many typeaheads let you control the html of individual result items by passing a string of html to render. That isn’t very Angular. It also doesn’t fully get me what I want since I need those ‘artist’ and ‘album’ sidebars which aren’t part of any single element. What I really want is an Angular template with all of the normal directives at my disposal. Then I want this html shoved into the typeahead control which can control the typeahead-y things like when its visible, selecting items, and what not.
So Angular has this idea of transclusion, which sounds quite fancy. And it is! Transclusion lets you use the html content of your custom component within the componenet template itself. This means I can have full control over the html of the typeahead while letting the shared behavior live within the typeahead component.
As an example, if I define my html as such:
And then the template of the
typeahead component is this:
The final result with a list of artists would be:
This is exactly what I want. I’m able to use the full power of Angular to create my list and the component takes care of the rest. The key thing to note is the
ng-transclude tag within the
typeahead template. This is where the inner html from the component will be shoved… er, transcluded.
You also have to configure the componenet directive to use transclusion.
At this point I have the html I’m after, but unfortunately it doesn’t do anything. I need the html and the
typeahead component to talk to each other so that when the user types something the proper item in the list is highlighted, or if the user clicks on an item in the list it is selected.
When the user types
black the first item in the list is highlighted (Black Eyed Peas). They then press
down and the second item in the list is highlighted (Black Sabbath) and the first one loses highlight. This is the behavior you would expect out of a typeahead.
The issue is that the
typeahead component places no constraints on the structure of the rendered list so it has no idea which item model corresponds to which html element. We need a way to link the items in the list together so that they can communicate on which one needs to be highlighted.
One way to accomplish this is with a directive controller. The typeahead component (which is a directive in case that isn’t clear) can declare a controller which other directives can request access to. This controller allows communication between the directives – namely which item is currently highlighted.
The directive responsible for each typeahead item is called
typeahead-item (yeah, I’m great at names), which is placed on each selectable item within the list like so:
typeahead-item directive links a given model (the artist) to the corresponding html (the
li element). The
typeahead component keeps track of which item is currently highlighted and the
typeahead-item directives watch this value with a comparison on their own item and act accordingly. In my case the action is just adding or removing a class.
Heres the code:
The directive requires the
typeahead controller. The
require: '^typeahead' instructs Angular to look on parent elements until it finds the controller.
The model item that is assigned to the directive (
typeahead-item="artist") is captured as a local variable. A watch is created on whether the item is
active, which means
highlighted (I apparently lack consistent naming), and sets a class accordingly. This class could be made configurable pretty easily but its fine for now.
There are then two event listeners which communicate back to the controller when the item is clicked on (select it) or hovered over (activate / highlight it).
The end result is that the component is able to highlight and select items attached to any arbitrary html elements. Success! The actual
typeahead component contains a bunch of code in its linker / controller but its pretty basic typeahead stuff and agnostic to how the list is rendered.
I love that customizing the look of the typeahed list consists of writing the same type of Angular code I use to customize how anything looks. I don’t have to learn the magic configuration language of a plugin. Win.
Here is the final template with all the nitty-gritty of the
typeahead component wired up.
Gist of the code.
And with results:
Its like the last.fm one but uglier. Mission accomplished.