Once you're using Backbone to handle your data, it's natural to want to use widgets that speak Backbone. All your end-points will be returning JSON objects for Backbone consumption already. What I also like about the Backbone philosophy is libraries that make very few assumptions about what you want to do with your objects. Backbone just sets up your objects and then gets our of your way and lets you access and customize the underlying objects in a conventional way.

What I don't like about JQuery-UI's autocomplete is that it made too many assumptions about the widget's behaviour in order to get common use cases up and running quickly. But your endpoints need to return a particular style of JSON and you have to conform to that. Arbitrary JSON will not do. Additionally if you want to do something as simple as customizing the line-item view, you have to override a "private" method. Even Twitter's typeahead.js (which does a ton of other awesome stuff) is not easy to customize to deal with arbitrary JSON.

I wanted to have an autocomplete plugin that just uses standard Backbone Collections and Views and let's me access them directly to customize as I would normally. I realized that autocomplete is really just a special case of a simple list view that is also a common pattern in Backbone. So I created a base InteractiveList View class that renders a Collection and attaches a click handler to each item. Then I created an Autocomplete View which attaches to the input element and creates it's own InteractiveList (unless one is provided). It basically just adds its own default click handler to do the normal event of populating the form input with the selected item's value. This class then also sets up standard keyboard navigation. It has almost the full set of features of the jquery-ui version but is much easier to customize. With Backbone we can deal with JSON using the same tools we already know and love.

If you want to use a jquery-style $('element').autocomplete(), you can do so by attaching the Backbone View using some plugin glue.

Here's the source. It's quite lean.

Examples:

Simple List

Simply display a Collection and use the attached template and click handler for each line item. The view can take an existing Collection or a url parameter, in which case it will create it's own Collection using Backbone.fetch().

Show/Hide source
<button id="simple-list">Show List</button>
  <div id="simple-results"></div>
  <script>
  simpleList = new Backbone.InteractiveList({
    collection: countries,
    el: $('#simple-results'),
    template: _.template('<p><%= name %></p>'),
    limit: 5,
    click: function(model, i) {
      this.$el.empty();
      alert('clicked ' + model.get('abbreviation') +' (' + i + ')');
    },
  });
  
  $('button#simple-list').on('click', function() { simpleList.render() });
  </script>

Filtered List

Same thing except pass a filter method to narrow the results and add some template code to hilite the matched phrase.

Show/Hide source
asdf asdf asdf asdf asdf asdfa sdf asdf asdf asdf asd fasdfasdf asdf asdf asdf asd fasdf asdf
<form id="filtered-list">
  <input name="search" autocomplete="off">
  <input type="submit" value="Get List">
</form>
<div id="filtered-results"></div>
<script>
  filteredList = new Backbone.InteractiveList({
    collection: countries,
    el: $('#filtered-results'),
    template: _.template('<p><%= name.replace(new RegExp("(" + $("form#filtered-list input[name=search]").val() + ")", "i") ,"<b>$1</b>") %></p>'),
    limit: 5,
    filter: function(model) {
      return model.get('name').toLowerCase().indexOf($('form#filtered-list input[name=search]').val().toLowerCase())!=-1;
    },
    click: function(model, i) {
      this.$el.empty();
      alert('clicked ' + model.get('abbreviation') +' (' + i + ')');
    },
  });
  
  $('form#filtered-list').on('submit', function(e) { e.preventDefault(); filteredList.render(); return false; });
</script>

Autocomplete

The Autocomplete View takes the input element and sets up an InteractiveList View for the results using the provided element. This View can be accessed by autocompleteList.resultsView if we want to customize it. It then attaches the 2 views together with a standard filter and sets up the navigational events.

Show/Hide source
<form id="autocomplete-list">
  <input name="search" autocomplete="off" style="width: 200px">
  <div class="autocomplete-results"></div>
</form>

<script>
  autocompleteList = new Backbone.AutocompleteList({
    collection: countries,
    el: $('form#autocomplete-list input[name=search]'),
    results: $('form#autocomplete-list .autocomplete-results'),
    template: _.template('<p><%= name.replace(new RegExp("(" + $("form#autocomplete-list input[name=search]").val() + ")", "i") ,"<b>$1</b>") %></p>'),
    delay: 0,
    minLength: 1,
    value: function(model) { return model.get('name') },
  });
</script>

Autocomplete using remote Collection

Same thing except using a Backbone Collection available on a remote url. The url is provided as a method that sends the search term, which Backbone allows. Here we use last.fm's JSONP service, demonstrating how the autocomplete can be as flexible as Backbone itself. We override the InteractiveList's collection.parse method to handle the last.fm data.

Show/Hide source
<form id="autocomplete-remote">
  <input name="search" autocomplete="off" style="width: 200px">
</form>

<script>
  autocompleteRemote = new Backbone.AutocompleteList({
    url: function() { return 'http://ws.audioscrobbler.com/2.0/?method=artist.search&api_key=cef6d600c717ecadfbe965380c9bac8b&format=json&' + $.param({ artist: $('form#autocomplete-remote input[name=search]').val() }); },
    filter: null,
    el: $('form#autocomplete-remote input[name=search]'),
    template: _.template('<p><%= name.replace(new RegExp("(" + $("form#autocomplete-remote input[name=search]").val() + ")", "i") ,"<b>$1</b>") %></p>'),
    delay: 500,
    minLength: 3,
    value: function(model) { return model.get('name') },
  }).resultsView.collection.parse = function(resp) {
    return resp.results.artistmatches.artist;
  };
</script>

Autocomplete replacement for jquery-ui version

With a little jquery plugin glue, it can be a drop-in replacement (more or less) for $.fn.autocomplete if we want to use it that way.

Show/Hide source
<form>
  <input id="jq-autocomplete" autocomplete="off">
</form>

<script>
$.fn.autocomplete = function(options) {
  return this.each(function() {
    var $this = $(this);
    if (!$this.data('autocomplete')) {
      $this.data('autocomplete', data = new Backbone.AutocompleteList(_.defaults(options || {}, { el: this })));
    }
  });
};

$('#jq-autocomplete').autocomplete({
  url: function() { return '/assets/sample.json?' + $.param({ term: $('#jq-autocomplete').val() }); },
  filter: null,
  template: _.template('<p><%= name.replace(new RegExp("(" + $("#jq-autocomplete").val() + ")", "i") ,"<b>$1</b>") %></p>'),
  delay: 500,
  minLength: 3,
  value: function(model) { return model.get('name') },
});
</script>


blog comments powered by Disqus

Published

09 March 2013

Tags