Skip to content

Embed URL

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Bootstrap's Typeahead plugin extended (allowing for AJAX functionality) among other things

This is a fork of Bootstrap Typeahead that adds minimal but powerful extensions.

For example, process typeahead list asynchronously and return objects

  # This example does an AJAX lookup and is in CoffeeScript
  $('.typeahead').typeahead(
    # source can be a function
    source: (typeahead, query) ->
      # this function receives the typeahead object and the query string
      $.ajax(
        url: "/lookup/?q="+query
        # i'm binding the function here using CoffeeScript syntactic sugar,
        # you can use for example Underscore's bind function instead.
        success: (data) =>
          # data must be a list of either strings or objects
          # data = [{'name': 'Joe', }, {'name': 'Henry'}, ...]
          typeahead.process(data)
      )
    # if we return objects to typeahead.process we must specify the property
    # that typeahead uses to look up the display value
    property: "name"
  )

For example, process typeahead list synchronously and fire a callback on selection

  // This example is in Javascript, collects html in some li's and returns it
  $('.typeahead').typeahead({
    source: function (typeahead, query) {
      var return_list = []
      $("li").each(function(i,v){
        return_list.push($(v).html())
      })
      // here I'm just returning a list of strings
      return return_list
    },
    // typeahead calls this function when a object is selected, and
    // passes an object or string depending on what you processed, in this case a string
    onselect: function (obj) {
      alert('Selected '+obj)
    }

  })

and a very simple example, showing you can pass list of objects as source, and get that object via onselect

  $('.typeahead').typeahead({
    // note that "value" is the default setting for the property option
    source: [{value: 'Charlie'}, {value: 'Gudbergur'}, ...],
    onselect: function(obj) { console.log(obj) }
  })

Note that onselect works without source as a function and vice versa. Events may be a cleaner solution to passing callbacks and using bind all over the place, but I tried to strike a balance between modifying the core source too much and adding functionality, so until further improvements on the original Typeahead source I think these additions are very helpful.

Update 02/23/2012: Fixed a bug

Gudbergur Erlendsson, reach me here or gudbergur at gmail

/* =============================================================
* bootstrap-typeahead.js v2.0.0
* http://twitter.github.com/bootstrap/javascript.html#typeahead
* =============================================================
* Copyright 2012 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ============================================================ */
!function( $ ){
"use strict"
var Typeahead = function ( element, options ) {
this.$element = $(element)
this.options = $.extend({}, $.fn.typeahead.defaults, options)
this.matcher = this.options.matcher || this.matcher
this.sorter = this.options.sorter || this.sorter
this.highlighter = this.options.highlighter || this.highlighter
this.$menu = $(this.options.menu).appendTo('body')
this.source = this.options.source
this.onselect = this.options.onselect
this.strings = true
this.shown = false
this.listen()
}
Typeahead.prototype = {
constructor: Typeahead
, select: function () {
var val = JSON.parse(this.$menu.find('.active').attr('data-value'))
, text
if (!this.strings) text = val[this.options.property]
else text = val
this.$element.val(text)
if (typeof this.onselect == "function")
this.onselect(val)
return this.hide()
}
, show: function () {
var pos = $.extend({}, this.$element.offset(), {
height: this.$element[0].offsetHeight
})
this.$menu.css({
top: pos.top + pos.height
, left: pos.left
})
this.$menu.show()
this.shown = true
return this
}
, hide: function () {
this.$menu.hide()
this.shown = false
return this
}
, lookup: function (event) {
var that = this
, items
, q
, value
this.query = this.$element.val()
if (typeof this.source == "function") {
value = this.source(this, this.query)
if (value) this.process(value)
} else {
this.process(this.source)
}
}
, process: function (results) {
var that = this
, items
, q
if (results.length && typeof results[0] != "string")
this.strings = false
this.query = this.$element.val()
if (!this.query) {
return this.shown ? this.hide() : this
}
items = $.grep(results, function (item) {
if (!that.strings)
item = item[that.options.property]
if (that.matcher(item)) return item
})
items = this.sorter(items)
if (!items.length) {
return this.shown ? this.hide() : this
}
return this.render(items.slice(0, this.options.items)).show()
}
, matcher: function (item) {
return ~item.toLowerCase().indexOf(this.query.toLowerCase())
}
, sorter: function (items) {
var beginswith = []
, caseSensitive = []
, caseInsensitive = []
, item
, sortby
while (item = items.shift()) {
if (this.strings) sortby = item
else sortby = item[this.options.property]
if (!sortby.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
else if (~sortby.indexOf(this.query)) caseSensitive.push(item)
else caseInsensitive.push(item)
}
return beginswith.concat(caseSensitive, caseInsensitive)
}
, highlighter: function (item) {
return item.replace(new RegExp('(' + this.query + ')', 'ig'), function ($1, match) {
return '<strong>' + match + '</strong>'
})
}
, render: function (items) {
var that = this
items = $(items).map(function (i, item) {
i = $(that.options.item).attr('data-value', JSON.stringify(item))
if (!that.strings)
item = item[that.options.property]
i.find('a').html(that.highlighter(item))
return i[0]
})
items.first().addClass('active')
this.$menu.html(items)
return this
}
, next: function (event) {
var active = this.$menu.find('.active').removeClass('active')
, next = active.next()
if (!next.length) {
next = $(this.$menu.find('li')[0])
}
next.addClass('active')
}
, prev: function (event) {
var active = this.$menu.find('.active').removeClass('active')
, prev = active.prev()
if (!prev.length) {
prev = this.$menu.find('li').last()
}
prev.addClass('active')
}
, listen: function () {
this.$element
.on('blur', $.proxy(this.blur, this))
.on('keypress', $.proxy(this.keypress, this))
.on('keyup', $.proxy(this.keyup, this))
if ($.browser.webkit || $.browser.msie) {
this.$element.on('keydown', $.proxy(this.keypress, this))
}
this.$menu
.on('click', $.proxy(this.click, this))
.on('mouseenter', 'li', $.proxy(this.mouseenter, this))
}
, keyup: function (e) {
e.stopPropagation()
e.preventDefault()
switch(e.keyCode) {
case 40: // down arrow
case 38: // up arrow
break
case 9: // tab
case 13: // enter
if (!this.shown) return
this.select()
break
case 27: // escape
this.hide()
break
default:
this.lookup()
}
}
, keypress: function (e) {
e.stopPropagation()
if (!this.shown) return
switch(e.keyCode) {
case 9: // tab
case 13: // enter
case 27: // escape
e.preventDefault()
break
case 38: // up arrow
e.preventDefault()
this.prev()
break
case 40: // down arrow
e.preventDefault()
this.next()
break
}
}
, blur: function (e) {
var that = this
e.stopPropagation()
e.preventDefault()
setTimeout(function () { that.hide() }, 150)
}
, click: function (e) {
e.stopPropagation()
e.preventDefault()
this.select()
}
, mouseenter: function (e) {
this.$menu.find('.active').removeClass('active')
$(e.currentTarget).addClass('active')
}
}
/* TYPEAHEAD PLUGIN DEFINITION
* =========================== */
$.fn.typeahead = function ( option ) {
return this.each(function () {
var $this = $(this)
, data = $this.data('typeahead')
, options = typeof option == 'object' && option
if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))
if (typeof option == 'string') data[option]()
})
}
$.fn.typeahead.defaults = {
source: []
, items: 8
, menu: '<ul class="typeahead dropdown-menu"></ul>'
, item: '<li><a href="#"></a></li>'
, onselect: null
, property: 'value'
}
$.fn.typeahead.Constructor = Typeahead
/* TYPEAHEAD DATA-API
* ================== */
$(function () {
$('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
var $this = $(this)
if ($this.data('typeahead')) return
e.preventDefault()
$this.typeahead($this.data())
})
})
}( window.jQuery );
/* =============================================================
* bootstrap-typeahead.js v2.0.0
* http://twitter.github.com/bootstrap/javascript.html#typeahead
* =============================================================
* Copyright 2012 Twitter, Inc.
@@ -29,6 +29,8 @@
this.highlighter = this.options.highlighter || this.highlighter
this.$menu = $(this.options.menu).appendTo('body')
this.source = this.options.source
+ this.onselect = this.options.onselect
+ this.strings = true
this.shown = false
this.listen()
}
@@ -38,8 +40,17 @@
constructor: Typeahead
, select: function () {
- var val = this.$menu.find('.active').attr('data-value')
- this.$element.val(val)
+ var val = JSON.parse(this.$menu.find('.active').attr('data-value'))
+ , text
+
+ if (!this.strings) text = val[this.options.property]
+ else text = val
+
+ this.$element.val(text)
+
+ if (typeof this.onselect == "function")
+ this.onselect(val)
+
return this.hide()
}
@@ -68,6 +79,25 @@
var that = this
, items
, q
+ , value
+
+ this.query = this.$element.val()
+
+ if (typeof this.source == "function")
+ value = this.source(this, this.query)
+ if (value)
+ this.process(value)
+ else
+ this.process(this.source)
+ }
+
+ , process: function (results) {
+ var that = this
+ , items
+ , q
+
+ if (results.length && typeof results[0] != "string")
+ this.strings = false
this.query = this.$element.val()
@@ -75,7 +105,9 @@
return this.shown ? this.hide() : this
}
- items = $.grep(this.source, function (item) {
+ items = $.grep(results, function (item) {
+ if (!that.strings)
+ item = item[that.options.property]
if (that.matcher(item)) return item
})
@@ -97,10 +129,14 @@
, caseSensitive = []
, caseInsensitive = []
, item
+ , sortby
while (item = items.shift()) {
- if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
- else if (~item.indexOf(this.query)) caseSensitive.push(item)
+ if (this.strings) sortby = item
+ else sortby = item[this.options.property]
+
+ if (!sortby.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
+ else if (~sortby.indexOf(this.query)) caseSensitive.push(item)
else caseInsensitive.push(item)
}
@@ -117,7 +153,9 @@
var that = this
items = $(items).map(function (i, item) {
- i = $(that.options.item).attr('data-value', item)
+ i = $(that.options.item).attr('data-value', JSON.stringify(item))
+ if (!that.strings)
+ item = item[that.options.property]
i.find('a').html(that.highlighter(item))
return i[0]
})
@@ -251,6 +289,8 @@
, items: 8
, menu: '<ul class="typeahead dropdown-menu"></ul>'
, item: '<li><a href="#"></a></li>'
+ , onselect: null
+ , property: 'value'
}
$.fn.typeahead.Constructor = Typeahead
@vanhalt

Outstanding... works like a charm!

This should be merged on Bootstrap. Thanks for this feature!

@foxtacles

Thanks a lot!

@pezholio

Brilliant, thanks!

@pezholio

Is there any way I can stop the first item from being automatically selected? At the moment the onselect event is fired when a user tabs out of the typeahead element.

@durus

The code is written by gudbergur and you can find it at https://gist.github.com/1866577 . I forked it in order to have a copy to work on if I need to do any changes.

@pezholio

Oops! Sorry! Will post it on the original Gist.

@cahofmeyr

I suggest adding a typing inactivity timeout, otherwise if an ajax request is used, it is fired on each keystroke and the search results box flashes as each ajax request completes.

Added to the 'default' case in the keyup function:

          if (this.inactivityTimer)
            window.clearTimeout(this.inactivityTimer);
          var that = this;
          this.inactivityTimer = window.setTimeout(function () {
            that.lookup();
          }, 500);
@cahofmeyr

Oops, looks like I made the same mistake as pezholio :-)

I noticed that the typing inactivity timer was already suggested on the original gudberger gist.

@oceanBT

very usefull, if there is an ID field that belongs to the text of the typeahead field. ( like the value of select/dropdown option )

http://therosiek.com/2012/04/bootstrap-typeahead-with-onselect/comment-page-1/

@kneidels

hey folks

looks very good.
using bootstrap 2.0.3
i am getting an error: "results is null" in bootstrap.js line 1639

which points to this line:

  return ~item.toLowerCase().indexOf(this.query.toLowerCase())

any ideas? Thanks!

@eXeDK

@kneidels you need to set the 'strings' to false and then set the property 'property' to the name of the object field which you want to be shown (and be matched on). Like so:

$('#typeahead').typeahead({
    strings : false,
    property : 'name',
    source : [{"id":"1","name":"First guy"},{"id":"2","name":"Some other guy"}]
});
@vincentjr

I'd like to add functionality where the typeahead does not execute an ajax request till after 2 keystrokes. Any recommendations?

@vincentjr

Sorry for the noob question, wasn't thinking. Added if( $(this).val().length >= 2 ) { } in the source function for anyone that was curious. Is this the right way or is there a better way?

@ghost
Unknown commented

I believe you can just set the minLength property of typeahead to 2.

@kneidels

@eXeDK - thanks belatedly for your reply.

i added what you suggested, but am still getting a similar error:
"item is undefined" - referring to the same line as i mentioned above in my previous post:

      return ~item.toLowerCase().indexOf(this.query.toLowerCase())

here is my code:



$('#mainSearchBox2').typeahead({
    strings : false,
    minLength: 3,
    property : 'StudentName',
    source: function(typeahead, query) {
        $.ajax({
            url: "/system/ajax_autoCompleteSearch.asp",
            dataType: "json",
            type: "GET",
            data: {
                max_rows: 15,
                query: query,
                ajax: 1
            },
            success: function(data) {
                var return_list = [], i = data.length;
                while (i--) {
                    return_list[i] = {id: data[i].StudentID, value: removenull(data[i].StudentName) };
                }
                typeahead.process(return_list);
            }
        });
    },
    onselect: function(obj) {
        $('[name="studentID"]').val(obj.id);
    }
});

function removenull(str) {
    var new_str = str;
    if (str == '') {
        new_str = str.replace('', "N/A");
    }
    else if (str == null) {
        new_str = "N/A";
    }

    return new_str;
}


and here is the sample of the data returned (confirmed via http request), when typing in "liz"

[{"StudentID":"634","StudentName":"Kirsh, Elizabeth"},{"StudentID":"689","StudentName":"Alex, Elizabeth"},{"StudentID":"734","StudentName":"Brer, Elizabeth"}]

Thanks!

@Diolor

I know that might hear stupid for a autocomplete thing, but is there any way to turn off the "match" query item with the array?

I want to use it with google geocode whose array results are not exactly the same (e.g. query= 18457, results from json 185 47 so the second is fine to be displayed but it's not the same to be on the results).

So basically use it as a input field snitcher, call the json stuff and whatever the results are, post them.

@brianwigfield

@Diolor define a matcher that always returns true

@ammancilla

Thanks a lot !!!

@nodesl

this is a nice tutorial. but i have a small issue. while i'm typing on the text box it just stuck my cursor for seconds. why is that ? every time i type it take few seconds to process the ajax. why is that ?

@tinchoel10

Accent results are NOT working!!!!!!

@Err0r404

Just for information $.browser has been removed from jQuery in 1.9.X
So on line 196 you should replace

if ($.browser.webkit || $.browser.msie) {
    this.$element.on('keydown', $.proxy(this.keypress, this))
}

by
if (/webkit/.test(navigator.userAgent.toLowerCase()) || /msie/.test(navigator.userAgent.toLowerCase())) {
this.$element.on('keydown', $.proxy(this.keypress, this))
}

Hope it will help someone

@Gramzivi

Thanx Err0r404, u just cured my headache :)

@dejurin

give work example with click event (go to url)

@JuanSomniis

Thanx Err0r404,you rock!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.