Lugo Labs

jQuery UI Autocomplete with Ruby on Rails

Demo

Our task today is to build an autocomplete text box for searching books. We’ll use the jQuery UI Autocomplete plugin for its simplicity and because most probably its files are already added to your project. The back end will be built (with love) in Ruby on Rails, but you can hook the framework of your choice.

As usual, you can find the source code of this tutorial at its github repo. For a demo, type any letter (e.g. h) in the text box above.

jQuery UI

Assuming we already have a Rails application setup, let’s add the jQuery UI. We can do that manually by downloading the needed files from jQuery UI website, or use the fantastic jquery-ui-rails gem. Let’s do the latter as it is very easy to implement. We add the gem to our Gemfile:

and run:

Now we add a reference in our application.js manifest:

js
//= require jquery-ui/autocomplete

Note how we only add the plugin we need here; if we had needed all plugins, we could refer them like so:

js
//= require jquery-ui

Next we add a reference to the stylesheet manifest file, application.css:

css
*= require jquery-ui/datepicker

The same logic about downloading only the needed files is valid here too. The gem takes care of downloading the necessary images as well (even though we won’t need them in this project).

Rails model

The book model has a title, author, price, and an image provided by amazon.com. Let’s create that by running the Rails’ scaffold generator:

then run the migration:

We can add some books data taken from Amazon, that you can copy and paste from the github page, and run the rake task:

With the books in our database, let’s create a method to search the books by title, or author.

ruby
# app/models/book.rb
def self.search(term)
  where('LOWER(title) LIKE :term OR LOWER(author) LIKE :term', term: "%#{term.downcase}%")
end

The SQL LOWER function is used to make our query case insensitive (in PostgreSQL we could use the ILIKE operator).

The controller

We’ll only use the index action for this project:

ruby
# app/controllers/books_controller.rb
def index
  respond_to do |format|
    format.html
    format.json { @books = Book.search(params[:term]) }
  end
end

When the action renders JSON, it calls the search class method we wrote earlier in the book model.

The views

We’ll write two views for our index function: HTML and JSON. The core of the HTML file looks like this:

html
<div class="books-search">
  <input type="text" id="books-search-txt" autofocus>
  <div class="results" id="books-search-results"></div>
</div>

We have added some CSS classes here that we’ll use later to make our text box look amazing. Next, let’s create the JSON view:

ruby
json.array!(@books) do |book|
  json.title        book.title
  json.author       book.author
  json.price        number_to_currency(book.price)
  json.image_url    book.image_url
end

We extract all the pieces of data we need to show on the search results. I love Jbuilder that comes built in with Rails, for its view separation so I’m happy to use that.

Next, time to hook up the JavaScript.

The JavaScript

We’ll create a small JavaScript file, books.js within assets’ javascripts folder. First we namespace our objects:

js
var app = window.app = {};

This way it will not conflict with other objects named the same. Then we create the function constructor:

js
app.Books = function() {
  this._input = $('#books-search-txt');
  this._initAutocomplete();
};

app.Books.prototype = {

};

Our constructor instantiates the input jQuery object, then calls the _initAutocomplete method which will set up the autocomplete plugin. The rest of the methods will be added to the prototype of the Books function, which allows them to not be repeated in every instance of the Books function we’ll create later (not relevant here as we’ll only have one Books object at a time).

Now let’s write the _initAutocomplete method:

js
_initAutocomplete: function() {
  this._input
    .autocomplete({
      source: '/books',
      appendTo: '#books-search-results',
      select: $.proxy(this._select, this)
    })
    .autocomplete('instance')._renderItem = $.proxy(this._render, this);
}

Here we make use of some of the jQuery UI autocomplete methods (the full list is here):

  • - source: points to our books resource, which will return the index.json.jbuilder file
  • - appendTo: a jQuery selector, which will allow us to isolate our autocomplete and style it accordingly
  • - select: a function to be called when the user makes a selection.
  • - _renderItem: a method that will create a list item of the dropdown list of results.

Let’s create those methods we proxied to above.

js
_select: function(e, ui) {
  this._input.val(ui.item.title + ' - ' + ui.item.author);
  return false;
}

So when the user select a book from the list, the title and the author of the book are added to the text box.

js
_render: function(ul, item) {
  var markup = [
    '<span class="img">',
      '<img src="' + item.image_url + '" />',
    '</span>',
    '<span class="title">' + item.title + '</span>',
    '<span class="author">' + item.author + '</span>',
    '<span class="price">' + item.price + '</span>'
  ];
  return $('<li>')
    .append(markup.join(''))
    .appendTo(ul);
}

We want to show the image as well as the title, author, and price of the book, so we code that into a nice markup with its own CSS classes. At the end we append the markup to the list item, which gets appended to the list.

Now let’s create an instance of our Books function within our index.html.erb file.

html
<script>
  $(function() {
    new app.Books;
  });
</script>

The CSS

Let’s make a nice text box with a lot of padding, big font and rounded corners:

css
.books-search input {
  width: 100%;
  font-size: 1em;
  padding: .5em 1.3em;
  background: transparent;
  border: 1px solid #ddd;
  border-radius: 2em;
}

.books-search input:focus {
  border-color: #A5CEF5;
}

The jQuery UI Autocomplete plugin comes with some styles which can look a bit dated. Let’s change that by styling some of its classes:

css
.books-search .results .ui-widget-content {
  background: #fff;
  font-size: .9em;
  padding: .5em 0;
  border-color: #ddd;
  box-shadow: 0 .1em .2em rgba(187, 187, 187, 0.64);
  line-height: 1.2;
  max-height: 20em;
  overflow: hidden;
  overflow-y: auto;
}

.books-search .results .ui-menu-item {
  font-family: 'Helvetica Neue', Helvetica, sans-serif;
  padding: .4em .6em .4em 6.2em;
  position: relative;
  border: 0;
  border-left: 5px solid transparent;
  position: relative;
  height: 6.5em;
}

Please note ui-widget-content is the class added to the dropdown list, whereas ui-menu-item’ is the class that decorates the list item. Their focus version isui-state-focus`, so let’s change that as well:

css
.books-search .results .ui-state-focus {
  background: #fff;
  border-left-color: #08c;
  font-weight: 300;
}

By isolating the classes within our books-search element, we make sure that our changes only affect the current autocomplete instance. If we wanted the changes to affect all plugins, we could remove the context .books-search .results.

Now it’s time to style the book elements within the list, i.e. the image, title, and price:

css
.books-search .results .ui-menu-item .img {
  position: absolute;
  width: 5em;
  height: 6em;
  top: .4em;
  left: .6em;
  overflow: hidden;
}

.books-search .results .ui-menu-item .img img {
  max-width: 100%;
}

.books-search .results .ui-menu-item .title {
  display: block;
  color: #555;
}

.books-search .results .ui-menu-item .author {
  display: block;
  font-size: .8em;
  text-transform: uppercase;
  color: #aaa;
  margin-top: .6em;
  letter-spacing: 1px;
}

.books-search .results .ui-menu-item .price {
  display: block;
  font-size: .8em;
  color: #aaa;
  margin-top: .5em;
}

.books-search .results .ui-state-focus .price,
.books-search .results .ui-state-focus .author,
.books-search .results .ui-state-focus .title {
  color: #08c;
}

That’s it. You can use the same method to implement the other jQuery UI plugins in your Rails projects (but not only). Please check out the full source code on github for all details.

Happy coding!