6

The bootstrap-typeahead-rails gem's README kicks the question over to Twitter's typeahead.js README. This left much to be desired.

This Stack Overflow answer provides detailed instructions for the twitter-typeahead-rails gem. I wanted to see something like that for the bootstrap-typeahead-rails gem.

Community
  • 1
  • 1
klenwell
  • 6,978
  • 4
  • 45
  • 84

2 Answers2

16

Here's my guide. It follows @ihaztehcodez's example. This example assumes a model Thing and adds a form to the index view for searching things by the model's name attribute.

A few notes:

  • I'm using Rails 4 (4.2.1).
  • For search queries, I'm using the Searchlight gem.
  • For templates, I'm using the slim-rails gem.
  • Styling is left as an exercise for the developer.

Add gem to gemfile

# Gemfile

# Typeahead gem
gem 'bootstrap-typeahead-rails'

# Optional gems
gem 'searchlight'
gem 'slim-rails'

Include typeahead files in asset manifests

Stylesheet (SASS)

# app/assets/stylesheets/application.scss

  *= require bootstrap-typeahead-rails

Javascript

# app/assets/javascripts/application.js

//= require bootstrap-typeahead-rails
//= require_tree .

Add typeahead route to routes file

# config/routes.rb

  get 'things/typeahead/:query' => 'things#typeahead'

Add typeahead javascript code

# app/assets/javascripts/things.js

var onReady = function() {

  // initialize bloodhound engine
  var searchSelector = 'input.typeahead';

  var bloodhound = new Bloodhound({
    datumTokenizer: function (d) {
      return Bloodhound.tokenizers.whitespace(d.value);
    },
    queryTokenizer: Bloodhound.tokenizers.whitespace,

    // sends ajax request to remote url where %QUERY is user input
    remote: '/things/typeahead/%QUERY',
    limit: 50
  });
  bloodhound.initialize();

  // initialize typeahead widget and hook it up to bloodhound engine
  // #typeahead is just a text input
  $(searchSelector).typeahead(null, {
    displayKey: 'name',
    source: bloodhound.ttAdapter()
  });

  // this is the event that is fired when a user clicks on a suggestion
  $(searchSelector).bind('typeahead:selected', function(event, datum, name) {
    //console.debug('Suggestion clicked:', event, datum, name);
    window.location.href = '/things/' + datum.id;
  });
};

Add relevant methods/actions to controller

# app/controllers/things_controller.rb

  # GET /things
  # GET /things.json
  def index
    @search = ThingSearch.new(search_params)
    @things = search_params.present? ? @search.results : Thing.all
  end

  # GET /things/typeahead/:query
  def typeahead
    @search  = ThingSearch.new(typeahead: params[:query])
    render json: @search.results
  end

  private

  def search_params
    params[:thing_search] || {}
  end

Add search form to index view (using SLIM gem)

# app/views/things/index.html.slim

div.search.things
  = form_for @search, url: things_path, method: :get do |f|
    div.form-group.row
      div.col-sm-3
      div.col-sm-6
        = f.text_field :name_like, {class: 'typeahead form-control',
            placeholder: "Search by name"}
        = f.submit 'Search', {class: 'btn btn-primary'}
      div.col-sm-3.count
        | Showing <strong>#{@things.length}</strong> Thing#{@things.length != 1 ? 's' : ''}

Create Searchlight search class

If you prefer not to use Searchlight, use the ActiveRecord query interface in the model.

# app/searches/thing_search.rb

class ThingSearch < Searchlight::Search
  search_on Thing.all

  searches :name_like, :typeahead

  # Note: these two methods are identical but they could reasonably differ.
  def search_name_like
    search.where("name ILIKE ?", "%#{name_like}%")
  end

  def search_typeahead
    search.where("name ILIKE ?", "%#{typeahead}%")
  end
end
Community
  • 1
  • 1
klenwell
  • 6,978
  • 4
  • 45
  • 84
  • So nice and thorough! Could you use `pluralize` in the `index` view on that last line? Great post. – steve klein May 03 '15 at 22:26
  • @steveklein I agree that line is ugly. I'd like to make number itself bold but not aware how I can do that with `pluralize` helper. Any recommendations? – klenwell May 03 '15 at 22:51
  • Oh I see what you mean. I'm not sure it is any better, but you could `split` the output of `pluralize` and just bold the number. Yours if fine though since by the time you get to the view, you presumably know you are dealing with "things" and not "oxen" (for ex). – steve klein May 03 '15 at 22:55
  • 1
    @klenwell from where does the `thing_search` in `def search_params params[:thing_search] || {} end` come from ? – Sooraj Jan 05 '16 at 14:56
  • @SoorajChandran Good question and good eye. `form_for search` in the index view should be `form_for @search`. (I've fixed code above.) In [my reference](https://github.com/klenwell/recruiters-on-rails/blob/master/app/views/recruiters/index.html.slim), the search form was in a partial so `@search` was passed in as `search`, hence the mistake. `thing_search` thus comes from that form, to which I believe Searchlight adds a little magic. You can find the real-world implementation that this was based on in my `recruiters_controller` [here](https://github.com/klenwell/recruiters-on-rails). – klenwell Jan 05 '16 at 15:26
  • 2
    Important note: `search_on` has been [removed](https://github.com/nathanl/searchlight/blob/v4.1.0/CHANGELOG.md#v400---2015-10-28) from Searchlight `4.0.0` and above. Instead you need to define the method `def base_query; Thing.all; end` – GMA Feb 04 '16 at 00:02
  • 2
    Oh, and you can also remove the line `searches :name_like, :typeahead` as of 4.0.0 - it's now inferred automatically based on the names of the instance methods within the `TypeSearch` class. And `search` within the methods should be `query`, e.g. `query.where("name ILIKE ?", "%#{name_like}%")` – GMA Feb 04 '16 at 00:03
  • Any solutions for `undefined local variable or method 'things_path'` error? – laimison Apr 23 '17 at 23:11
  • Hey @klenwell, I am currently busy trying to solve the same problem related to tying up `typeahead.js` with my users controller with the `autocomplete` method in it and I keep getting undefined for the `datum.id` where datum is passed as parameter. – Thato Sello Nov 15 '20 at 14:51
3

@klenwell's answer is out of date. Here's how I got it to work:

I'm using:

  • Bootstrap v3.3.6
  • bloodhound 0.11.1
  • bootstrap3-typeahead 3.1.0
  • jQuery 2.2.0

My model is called Destination.

app/models/destination_search.rb:

class DestinationSearch < Searchlight::Search

  def base_query
    Destination.all
  end

  def search_typeahead
    query.where("name ILIKE", "%#{typeahead}%")
  end

end

controller:

class DestinationsController < APIController

  def typeahead
    render json: DestinationSearch.new(typeahead: params[:query]).results
  end

end

JS:

var bloodhound = new Bloodhound({
  datumTokenizer: function (d) {
    return Bloodhound.tokenizers.whitespace(d.value);
  },
  queryTokenizer: Bloodhound.tokenizers.whitespace,

  remote: {
    url: '/api/destinations/typeahead?query=%QUERY',
    wildcard: "%QUERY",
  },
  limit: 10
});
bloodhound.initialize();

$(document).ready(function () {
  $(".destination-typeahead").typeahead({
    source: function (query, process) {
      return bloodhound.search(query, process, process);
    },
  });
});

and in the view:

<%= text_field_tag :destination, class: "destination-typeahead" %>

It feels a little bit hacky how I'm passing the process method into bloodhound.search twice - this is because bloodhound#search takes two callbacks as arguments, one that deals with cached/prefetched data and one that deals with data pulled dynamically via AJAX. I might not be using #search 100% correctly, but this approach works, and it's a simple start.

GMA
  • 5,816
  • 6
  • 51
  • 80
  • I'm a beginner at this and unclear how your correction fits into the original answer. Can you elaborate your view code please? how does legfields fit into klenwell's search form? – theawesome Feb 09 '16 at 11:13
  • Oops, `leg_fields` was something specific to my own codebase that's not relevant here. I've edited it out. My answer isn't designed to fit into klenwall's search form - you'll need to design your form depending on what exactly you're trying to save. – GMA Feb 09 '16 at 22:45
  • How `APIController` can be set up? This also has '/api' in url. It would be great to get more explanation. – laimison Apr 23 '17 at 22:46