Autocomplete with jQuery plugin for backbone.js on remote api

juin 28th, 2013

The goal of today’s tutorial is to create a simple autocomplete field with Backbonejs on a remote JSON-based API. Most Backbone applications are based on Underscore.js and use jQuery as their DOM manipulation. It’s generally easy to use as Backbone’s view provide direct access to a jQuery element such as « this.el » or « this.$el » and deal with the returned data appropriately. From there, we can call standard jQuery code and plugins.

The DataService API we’ll use in this tutorial is GeoNames (api.geonames.org), a processing service that translates geolocation information (zipcode, city, country, lat/long) between different formats (xml/json). This is extremely helpful if you are trying to build an API application that displays information on a map.

Few solutions are available on the net, but I needed somethings that fits the structure of backbonejs (plugins). My choice has focused on the Backbone Widgets plugin that will keep both the structure of the autocomplete with jquery and collections management with backbone.

Let’s get started

In order to use this tutorial, you must have a project ready to start with the following libraries :

Also, to make this work, you’ll need a server side to build html tags with text.js and a real account on the Geonames API site.

Here’s the main application structure :


-css/
  |__jquery.backbone.widgets.css
-js/              
  |__vendor  
  |  |__require.js (Module loader for javascript files)
  |  |__text.js (An API to embed HTML in a JavaScript file)
  |  |__jquery-1.9.1.min.js
  |  |__backbone-min.js
  |  |__underscore-min.js
  |  |__jquery.backbone.widgets.js (jQuery plugin for backbonejs to enable autocomplete)
  |__app
  |  |__citiesCollection.js
  |  |__cityModel.js
  |  |__cityView.js
  |__main.js
-index.html

Bootstrapping your application

An elegant and extensible way to improve the speed and quality of your code is to use the js file module loader require.js.

Require.js is an AMD (Asynchronous Module Definition) script loader that asynchronously loads your JavaScript to improve page load performance, while also providing the ability to organize your JavaScript into self contained modules (files). Each JavaScript file represents a module.

The method is to enclosed each module in a define tag that lists the module’s file dependencies to keep the global namespace free. Since none of your modules are global, inside of each module, you need to declare which other modules are dependencies and pass them to the current one. This provides a solution for limiting global variables and dependency management.

The initialization of the application and vendor library is made inside the main.js and call in the index.html files :


<script data-main="js/main" src="js/vendor/require.js"></script>

Require.js configuration file

Sets the require.js configuration for your application (main.js) :


(function() {
   'use strict';

   // Require.js allows us to configure shortcut alias
   require.config({
      paths: {
         jquery: 'vendor/jquery-1.9.1.min',
         underscore: 'vendor/underscore-min',
         backbone: 'vendor/backbone-min',
         autocomplete: 'vendor/jquery.backbone.widgets',
         text: 'vendor/text'
      },
      // The shim config allows us to configure dependencies for
      // scripts that do not call define() to register a module
      shim: {
         jquery: {
            exports: "$"
         },
         underscore: {
            exports: '_'
         },
         backbone: {
            deps: ['underscore', 'jquery'],
            exports: 'Backbone'
         },
         autocomplete: {
            deps: ['jquery'],
            exports: 'autocomplete'
         }
      }
   });

   require([
      'backbone',
      'app/city_view',
      'app/city_model',
      'autocomplete'
   ], function(Backbone, CityView, City) {
      // init app
      var cityView = new CityView({model: new City()});
      Backbone.history.start();
   });

}());

The collection

In backbone, the collection is a wrapper of a group of Models, in this case, City. Just tell at the first line, which model it’s wrapping.

Typically a collection comes with the ability to do a JSON call off to a remote server. So in the second line you should specify the URL of the Geonames API, from which it can pull JSON : « http://api.geonames.org/searchJSON ».

In our case, we’re overriding the function synch to force a jsonp call on the server. Appending callback=? to the end of the URL enables us to make cross-domain JSON AJAX calls. This method uses what’s called JSONP (or JSON with padding), which allows a script to fetch data from another server on a different domain.

Then the function parse handle the callback which passed the raw response object and should return the array of model attributes to be added to the collection. Also, we override the default JSON response to have better namespace in our responses.


define([          
   'backbone',  
   'app/city_model'
], function(Backbone, City) {
   'use strict';

   var CitiesCollection = Backbone.Collection.extend({
      model: City,
      url: "http://api.geonames.org/searchJSON",
                     
      initialize: function () {},

      // override backbone synch to force a jsonp call
      sync: function(method, model, options) {
         var params = _.extend({
            url: this.url,
            type: 'GET',
            dataType: 'jsonp',
            data: {
               featureClass: "P",                
               style: "full",
               maxRows: 15,
               name_startsWith: $('input[name=city]').val(),
               username: "physalix"
            }
         }, options);

         return $.ajax(params);
      },

      parse: function(response) {
         var city = {};  
         var self = this;
         
         $.map(response.geonames, function(item) {
            city.id              = item.geonameId;
            city.name            = item.name;
            city.region          = item.adminName1;
            city.country         = item.countryName;
            city.continentCode   = item.continentCode;
            city.geonameId       = item.geonameId;
            city.countryCode     = item.countryCode;
            city.latitude        = item.lat;
            city.longitude       = item.lng;
            city.population      = item.population;
            city.timezone        = item.timezone.timeZoneId;
            city.timezone_dst    = item.timezone.dstOffset;
            city.timezone_gmt    = item.timezone.timezone_gmt;
            city.label           = item.name + (item.adminName1 ? ", " + item.adminName1 : "") + ", " + item.countryName;
            city.infos           = "{\"continent_code\": \"" + item.continentCode + "\", \"country_code\": \"" + item.countryCode + "\", \"country_name\": \"" + item.countryName + "\", \"region\": \"" + item.adminName1 + "\", \"latitude\": " + item.lat + ", \"longitude\": " + item.lng + ", \"name\": \"" + item.name + "\", \"lower_name\": \"" + item.name.toLowerCase() + "\", \"population\": " + item.population + ", \"timezone\": \"" + item.timezone.timeZoneId + "\", \"timezone_dst\": " + item.timezone.dstOffset + ", \"timezone_gmt\": " + item.timezone.gmtOffset + ", \"geonameid\": \"" + item.geonameId + "\" }";

            self.push(city);
         });
                             
         return this.models;
      }

   });

   return CitiesCollection;

});

The model

The model needs an idAttribute to identify the object and we just declare fields we gonna need in the view.


define([
   'backbone'
], function(Backbone) {
   'use strict';

   var City = Backbone.Model.extend({
      idAttribute: "id",

      defaults: {
         label: '',
         value: '',
         infos: ''
      },

      label: function () {
         return this.get("label");
      },
      value: function () {
         return this.get("id");
      },
      infos: function () {
         return this.get("infos");
      }      
   });

   return City;

});

Now, we have our data ready to go. Let’s start with the Views…

The template

The template is rendered in the view with the text.js library. It uses « label » and « infos » variables declare in the model.


<label for="c2_city">City</label>
<div class="bbf-editor">
    <input type="text" id="c2_city" name="city" value="<%= label %>">
    <input type="hidden" id="c2_cityinfos" name="cityinfos" value="<%= infos %>">
</div>
<div class="bbf-help"></div>
<div class="bbf-error"></div>

The view

The view use the Backbone-widgets plugin autocomplete function to update html list.


define([
   'app/cities_collection',
   'text!app/cities.html'
], function(CitiesCollection, citiesTpl) {
   'use strict';

   var CityView = Backbone.View.extend({
      el: '#page',
      template: _.template(citiesTpl),

      events: {
         'focus #c2_city':    'citiesAutocomplete',
         'keydown #c2_city':  'invokefetch'
      },

      initialize: function() {
         this.citiesCollection = new CitiesCollection();
         this.render();
      },

      render: function() {
         this.$el.html(this.template(this.model.toJSON()));
         return this;
      },

      invokefetch : function() {
         this.citiesCollection.fetch();
         $("#c2_city").unbind( "keydown", this.invokefetch);
      },

      citiesAutocomplete: function () {
         var self = this;

        $('#c2_city').autocomplete({
          collection: self.citiesCollection,
          attr: 'label',
          noCase: true,
          onselect: self.autocompleteSelect,
          ul_class: 'autocomplete shadow',
          ul_css: {'z-index':1234},
          max_results: 15
        });
      },

      autocompleteSelect: function(model) {
         $('#c2_city').val(model.label());
         $('#c2_cityinfos').val(model.infos());
      }

   });

   return CityView;
});

Sources’s available on Github.

Hope this help !