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:
gem 'jquery-ui-rails'
and run:
bundle update
Now we add a reference in our application.js
manifest:
//= 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:
//= require jquery-ui
Next we add a reference to the stylesheet manifest file, application.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:
bin/rails g scaffold book title:string author:string price:decimal image_url:string --skip-javascripts --skip-stylesheets
then run the migration:
bin/rake db:migrate
We can add some books data taken from Amazon, that you can copy and paste from the github page, and run the rake task:
bin/rake db:seed
With the books in our database, let’s create a method to search the books by title, or author.
# 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:
# 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:
<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:
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:
var app = window.app = {};
This way it will not conflict with other objects named the same. Then we create the function constructor:
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:
_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 theindex.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.
_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.
_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.
<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:
.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:
.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 is
ui-state-focus`, so let’s change that as well:
.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:
.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!