Rails: Easily add autocomplete to forms

Intro

Since resources in my rails app can easily reach several thousand and million entries, I wanted a way to easily select one in a form. A quick google search led me to this excellent post by releu: http://gistflow.com/posts/428-autocomplete-with-rails-and-select2

While it was great some parts where missing for me, or had to do some additional effort. So I want to provide a 0-100 process to get it working.

Setup

Note: I am using Rails 4 with Bootstrap 3.

Releu recommended the awesome jQuery-based replacement for select boxes select2. Let’s install it. Modify your Gemfile to include the following:

# Gemfile
gem 'select2-rails'

# later we will take advantage of this gem
gem 'kaminari'

Kaminari is a paginator gem for rails.

Now edit the application.js and application.css.scss to include select2:

/* application.js */
//= require select2
/* application.css.scss */
 *= require select2

Usage

In your view, add a hidden field to the form:

= hidden_field_tag(:resource, '', id: 'res_select', class: 'select2 ajax', style: 'width: 100%;', data: { source: search_resource_names_path, placeholder: 'Search for a name' })
# or
= f.hidden_field :organization_id_eq, class: 'select2 ajax', data: { source: organizations_path }

The important parts are

  • It is hidden because select2 will create it’s own structure. So if you make it a visible field, it will look probably just ugly.
  • The classes select2 and ajax are required.
  • Make sure to create the source path in your config/routes.rb. This is where the ajax calls will be made to.
  • I added style: 'width: 100%;', because you cannot use Bootstrap’s form-control class. That is, because select2 copies all CSS classes and styles to a container div and not the resulting input field.

To enable select2, the .select2(options) method has to be called on such elements. The following was adapted from releu:

# your controller.js.coffee
$(document).ready ->
  $('.select2').each (i, e) =>
    select = $(e)
    options =
      placeholder: select.data('placeholder')

    if select.hasClass('ajax') # only add ajax functionality if this class is present
      options.ajax =
        url: select.data('source')
        dataType: 'json'
        data: (term, page) ->
          q: term
          page: page
          per: 25
        results: (data, page) ->
          results: data.resources
          more: data.total > (page * 25) # adding the more: option enables infinite scrolling (select2 will load more content if available)

      options.dropdownCssClass = "bigdrop"

    select.select2(options)

Last but not least, implement the controller method as pointed to in your routes file:

  def search_for_name
    @resources = Resource.select([:id, :name]).
                          where("name like :q", q: "%#{params[:q]}%").
                          order('name').page(params[:page]).per(params[:per]) # this is why we need kaminari. of course you could also use limit().offset() instead

    # also add the total count to enable infinite scrolling
    resources_count = Resource.select([:id, :name]).
                          where("name like :q", q: "%#{params[:q]}%").count

    respond_to do |format|
      format.json { render json: {total: resources_count, resources: @resources.map { |e| {id: e.id, text: "#{e.name} (#{e.username})"} }} }
    end
  end

That’s it.