#102 Auto-Complete Association (revised)
- Download:
- source codeProject Files in Zip (95.6 KB)
- mp4Full Size H.264 Video (16.9 MB)
- m4vSmaller H.264 Video (9.6 MB)
- webmFull Size VP8 Video (11.9 MB)
- ogvFull Size Theora Video (21.5 MB)
Say that we have a simple application that manages a number of products, each of which belongs to a category. To add a product to this app we have a form, which is shown below, that has a dropdown list for selecting the new product’s category.
This approach works well for a limited number of categories, but dropdown lists can quickly become cumbersome when there is a large number of options to choose from. In this episode we’re going to replace this dropdown with a text field that will autocomplete when a user starts to enter the name of a category.
The template that renders the form is shown below. It uses collection_select
to create the dropdown for the category association.
<%= form_for(@product) do |f| %> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :price %><br /> <%= f.text_field :price %> </div> <div class="field"> <%= f.label :category_id %><br /> <%= f.collection_select :category_id, Category.order(:name), :id, :name, include_blank: true %> </div> <div class="actions"> <%= f.submit %> </div> <% end %>
We’ll replace the collection_select
with a text field and call it category_name
, as that’s what the user will be typing in here.
<div class="field"> <%= f.label :category_name %><br /> <%= f.text_field :category_name %> </div>
If we try reloading the form now we’ll see an error message as the Product
model doesn’t have a category_name
attribute. To fix this we’ll create a virtual attribute.
class Product < ActiveRecord::Base belongs_to :category def category_name category.try(:name) end def category_name=(name) self.category = Category.find_by_name(name) if name.present? end end
We’ve created getter and setter methods for category_name
. The getter returns the associated Category
’s name (but note that we use try
so that the it returns nil
if there is no associated category). The setter sets the product’s category
to the Category
with a matching name if the name
is present.
When we reload the form now we have a category name field and if we add a product with an existing category name the product is added to the database with its category association set.
What should happen, though, if someone creates a product with a new category? It would be good if the new category was created along with the product in these cases and Rails makes this easy to do. All we need to do is replace find_by_name
in the setter with find_or_create_by_name
.
def category_name=(name) self.category = Category.find_or_create_by_name(name) if name.present? end
Now when we add a product with a new category the category is created too.
Adding Auto-completion
What we really want to do is add auto-completion so that as the user types in the category field the categories that match what has been typed are shown. One of the easiest ways to do this in a Rails 3.1 application is to use jQuery UI which comes with an Autocomplete widget so we’ll take this approach.
JQuery UI is already available in a Rails 3.1 application. To use it we just need to require it in the application.js
manifest file.
// This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript/Coffee code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. // //= require jquery //= require jquery-ui //= require jquery_ujs //= require_tree .
We’ll add the autocomplete functionality in the products CoffeeScript file.
jQuery -> $('#product_category_name').autocomplete source: ['foo', 'food', 'four']
In this code we first check that the DOM has loaded and when it has we find our category name field by its id
and call autocomplete
on it. This function takes a source
option that determines where the autocomplete options come from. We can pass it either a URL, which will make an AJAX request and use what’s returned, or an array of options. To get things working quickly we’ve used an array for now.
When we go to the new product page now and start typing something that matches one or more of the items in the array we’ll see the autocomplete list.
The autocomplete list works but it looks terrible. We’ll improve this by adding the following code to the products SCSS file.
ul.ui-autocomplete { position: absolute; list-style: none; margin: 0; padding: 0; border: solid 1px #999; cursor: default; li { background-color: #FFF; border-top: solid 1px #DDD; margin: 0; padding: 0; a { color: #000; display: block; padding: 3px; } a.ui-state-hover, a.ui-state-active { background-color: #FFFCB2; } } }
When we reload the page now the list looks much better.
Getting Autocomplete Values From The Database
Now that our list looks good we can concentrate on populating it with the matching categories from the database instead of the values from the array. There are two ways we can do this and we’ll show you both approaches here.
The first option keeps everything on the client. This works well if there aren’t a large number of options to choose from as is the case here. What we do is embed all of the options in a data attribute on the category name text field.
<div class="field"> <%= f.label :category_name %><br /> <%= f.text_field :category_name, data: {autocomplete_source: Category.order(:name).map(&:name)} %> </div>
To do this we set a data
option on the text field (this data
hash is new in Rails 3.1 and is a convenient way to set data attributes) and pass it an array of category names. If we reload the page and view the source we’ll see what this has done.
<div class="field"> <label for="product_category_name">Category name</label><br /> <input data-autocomplete-source="["Beverages","Board Games","Books","Breads","Canned Foods","Clothes","Computers","Dry Foods","Frozen Foods","Furniture","Headphones","Magazines","Music","Other Electronics","Pastas","Portable Devices","Produce","Snacks","Software","Televisions","Toys","Video Games","Video Players","Videos"]" id="product_category_name" name="product[category_name]" size="30" type="text" /> </div>
The text field now has a data-autocomplete-source
attribute which contains the categories. These have been converted to a JSON string and HTML escaped. We can now replace the dummy data in autocomplete
with the data from this attribute.
jQuery -> $('#product_category_name').autocomplete source: $('#product_category_name').data('autocomplete-source')
When we reload the page now and enter text in the category name field the matching categories appear in the autocomplete list.
This is all we have to do to get autocompletion working on the client and this approach works perfectly for us as we only have a limited number of categories. There are other situations, however, where we could have hundreds or even thousands of potential options to choose from and having every option on the client would be impractical. In these cases its better to use an AJAX request to fetch the autocomplete options from the server instead of embedding them in the HTML document.
To do this we pass a URL to the autocomplete
function instead of an array. We’ll replace the data in the data-autocomplete-source
attribute with a URL. As the AJAX request should return a list of categories we’ll use the categories_path
URL.
<div class="field"> <%= f.label :category_name %><br /> <%= f.text_field :category_name, data: {autocomplete_source: categories_path} %> </div>
We don’t have a CategoriesController yet so we’ll create one now.
$ rails g controller categories
In order for the categories_path
method to be defined we’ll modify the routes file and add a categories
resource.
Store::Application.routes.draw do root to: 'products#index' resources :products resources :categories end
Now in our new CategoriesController
we’ll write the index
action. This will fetch the categories that match what’s been typed into the text field and return them as JSON data.
class CategoriesController < ApplicationController def index @categories = Category.order(:name).where("name like ?", "#{params[:term]}") render json: @categories.map(&:name) end end
The autocomplete widget passes in the text from the text
field as a term parameter so we use that parameter to filter the categories. We then return this filtered list as an array of names.
When we reload the page and start typing into the category name field now the matching categories are fetched from the server.
The autocomplete list is a little slower as an AJAX request is made each time the text in the text field changes, but we’re no longer having to send the complete list of categories to the client each time the page loads.
That’s it for this episode on using autocompletion with an association. For more details about how it works and the options that can be used it’s well worth reading the documentation. For example we can pass in a minLength option that determines the minimum number of characters that we need to enter before the AJAX request is made and so on.
If you need autocompletion for a many-to-many relationship then take a look at episode 258 which does just this.