Typeahead with Bloodhound

What's ahead

 

  • Setting up a remote source
     
  • Installing Typeahead
     
  • Getting End-point data
     
  • Wire up to View
     
  • Demo
     
  • Try it yourself

Setting up a remote source

  • Choose favorable end-point
  • Set-up reverse proxy to API

Choose favorable end-point

  • Mean response time should be ~< 200ms
  • Acceptable rate limiting on end-point

 

In other words, look for something like Elastic Search with a generous rate limit. Tools like Compose.io offer hosted solutions.

Set-up reverse proxy to API

Hapi = require('hapi')
path = require('path')

exports.startServer = (port, publicPath, callback) ->

  # Start server on port specified in brunch-config
  server = new Hapi.Server port

  # Reverse proxy to Untappd API
  server.route
    method: '*'
    path: '/api/untappd/search/{path*}'
    handler:
      proxy:
        mapUri: (request, callback) ->
          url = "http://api.untappd.com/v4/search/#{request.params.path}?client_id=CLIENT_ID&client_secret=CLIENT_SECRET&q=#{request.query.q}"
          callback null, url
        redirects: 5
        timeout: 5000

  # Serve static files
  server.route
    method: '*'
    path: '/{path*}'
    handler:
      directory:
        path: publicPath

  # Start up server
  server.start ->
    console.log "Server running at #{server.info.uri}"

Getting End-point data

  • Create domain models
  • Create search model
  • Create search controller

Create domain models

Model = require('models/base/model')

module.exports = class Beer extends Model

###

Collection = require('models/base/collection')
Beer = require('./beer')

module.exports = class Beers extends Collection
  url: "/api/untappd/search/beer"
  model: Beer

  parse: (data, options) ->
    {items} = data.response.beers
    (beer for beer in _.pluck items, 'beer')

Create search model

Beers = require('models/beers')
 
# @requires {Component} backbone.nested-types
module.exports = NestedTypes.Model.extend
  defaults:
    results: Beers
 
  initialize: (attrs, opts) ->
    {@query} = opts

Create search controller

Typeahead = require('views/site/typeahead')
BeerSearch = require('models/beer-search')
BeerCollectionView = require('views/beer/beer-collection-view')
 
module.exports = class SearchController extends Controller

  beforeAction: (params, route, options) ->
    @search = new BeerSearch [], {query: options.query}
    @reuse 'typeahead', Typeahead, {collection: @search.results, region: 'secondary'}

  index: ->
    @view = new BeerCollectionView {collection: @search.results, region: 'main'}
 

Wire up to view

  • Configure Bloodhound
  • Extend a typeahead control
  • Style to your liking

Installing Typeahead

bower install --save typeahead.js

bower typeahead.js#*            cached git://github.com/twitter/typeahead.js.git#0.10.5
bower typeahead.js#*          validate 0.10.5 against git://github.com/twitter/typeahead.js.git#*
bower typeahead.js#~0.10.5     install typeahead.js#0.10.5

typeahead.js#0.10.5 bower_components/typeahead.js
└── jquery#2.0.3

Configure Bloodhound

View = require('views/base/view')

module.exports = class TypeaheadView extends View
  className: 'typeahead-view'
  template: require('./templates/typeahead')
  events:
    'submit form': '_onFormSubmit'
  listen:
    'router:match mediator': '_onRouterMatch'

  # @require {Component} typeahead.js
  initialize: (attrs) ->
    super
    do =>
      filter = (data) =>
        @collection.reset @collection.parse data
        ({name} for name in @collection.pluck 'beer_name')
      @engine = new Bloodhound
        datumTokenizer: Bloodhound.tokenizers.obj.whitespace('name'),
        queryTokenizer: Bloodhound.tokenizers.whitespace,
        dupDetector: (remoteMatch, localMatch) ->
          remoteMatch.name == localMatch.name
        limit: 50
        prefetch: {filter, url: @collection.url}
        remote: {filter, url: "#{@collection.url}?q=%QUERY"}
      @engine.initialize()

Extend a typeahead control

attach: ->
  super
  @$form = @$('#searchform')
  @$searchInpt = @$('input#search')
  @$searchInpt.typeahead
    minLength: 1
  ,
    name: 'beers' # appended to tt-dataset- to form class name of containing element
    displayKey: 'name'
    source: @engine.ttAdapter()
  .select()
  .on('typeahead:selected', @_onTypeaheadSelected.bind @)
  .on('typeahead:autocompleted', @_onTypeaheadAutocompleted.bind @)
  .keydown @_onTypeaheadKeydown.bind @

Style to your liking

/**
 * Twitter Typeahead
 * https://github.com/twitter/typeahead.js/blob/master/doc/jquery_typeahead.md#look-and-feel
 */

$border-color-Typeahead: #D6DCE3;
$border-radius-Typeahead: 5px;
$highlight-color-Typeahead: #4183C4;
$hint-color-Typeahead: #999;

.twitter-typeahead {
  display: block !important;
  max-width: 100%;
}

.tt-dropdown-menu {
  display: none !important; /* Hide Typeahead drop-down */
}

Demo

Try it yourself!

(requires Untapped API access)

git clone https://github.com/jhabdas/hopstop.git && cd $_
git checkout -b ratchet origin/ratchet
npm install
jake server:dev
Made with Slides.com