Twitter Bootstrap Typeahead - Id & Label

I'm using Bootstrap 2.1.1 and jQuery 1.8.1 and trying to use Typeahead's functionality.

I try to display a label and use an id like a standard <select />

Here is my typeahead initialization:

$(':input.autocomplete').typeahead({
    source: function (query, process) {
        $('#autocompleteForm .query').val(query);
        return $.get(
            $('#autocompleteForm').attr('action')
          , $('#autocompleteForm').serialize()
          , function (data) {
              return process(data);
          }
        );
    }
});

Here is the kind of JSON that I'm sending

[{"id":1,"label":"machin"},{"id":2,"label":"truc"}]

How can I tell process() to display my labels and store the selected ID in another hidden field?

Answers


There's a great tutorial here that explains how to do this: http://tatiyants.com/how-to-use-json-objects-with-twitter-bootstrap-typeahead/ (read my comment on that page if it hasn't been reflected yet in the main part of the post).

Based on that tutorial, and the JSON you provided, you can do something like this:

$(':input.autocomplete').typeahead({
    source: function(query, process) {
        objects = [];
        map = {};
        var data = [{"id":1,"label":"machin"},{"id":2,"label":"truc"}] // Or get your JSON dynamically and load it into this variable
        $.each(data, function(i, object) {
            map[object.label] = object;
            objects.push(object.label);
        });
        process(objects);
    },
    updater: function(item) {
        $('hiddenInputElement').val(map[item].id);
        return item;
    }
});                    

As of version 0.10.1 of Twitter Typeahead (https://github.com/twitter/typeahead.js), Id / Label is supported natively:

  $('input[name=address]').typeahead({
        hint: false
    }, {
        source: function (query, cb) {
            $.ajax({
                url: '/api/addresses?q=' + encodeURIComponent(query),
                dataType: 'json',
                cache: false,
                type: 'GET',
                success: function (response, textStatus, jqXHR) {
                    cb(response.data);
                },
                error: function (jqXHR, textStatus, errorThrown) {
                }
            });
        },
        name: 'addresses',
        displayKey: 'text'
    }).on('typeahead:selected', function (e, suggestion, name) {
        window.location.href = '/' + suggestion.id;
    });

If the example above, I'm passing an array of objects to the source callback (cb). By specifying displayKey: 'text', I'm telling the library to use the 'text' property for the auto-suggest. When the 'typeahead:select' callback is called, the second argument passed in (suggestion) contains the object that was selected.


To clarify what I was saying in my comment. If you wanted multiple type aheads on the same page you need to define each in a function and create a separate map variable for them.

function initFromField() {
    var map;
    $('#from:input.autocomplete').typeahead({
        source: function(query, process) {
            map = {};
            var data = [{"id":1,"label":"machin"},{"id":2,"label":"truc"}] // Or get your JSON dynamically and load it into this variable
            objects = constructMap(data, map);
            process(objects);
        },
        updater: function(item) {
            $('#hidden-from-input').val(map[item].id);
            return item;
        }
    });
}

function initToField() {
    var map;
    $('#to:input.autocomplete').typeahead({
        source: function(query, process) {
            objects = [];
            map = {};
            var data = [{"id":1,"label":"machin"},{"id":2,"label":"truc"}] // Or get your JSON dynamically and load it into this variable
            objects = constructMap(data, map);
            process(objects);
        },
        updater: function(item) {
            $('#hidden-to-input').val(map[item].id);
            return item;
        }
    });
}

function constructMap(data, map) {
    var objects = [];
    $.each(data, function(i, object) {
        map[object.label] = object;
        objects.push(object.label);
    });
    return objects;
}

$(function initFields() {
    initFromField();
    initToField();
});

Note how I scoped the map variable inside the two field initialization functions. This is important, it makes sure the same map variable is not used by both input fields.


The problem I have seen with some of these solutions, is that the source function is called repeatedly on every keyup event of the input box. Meaning, the arrays are being built and looped over on every keyup event.

This is not necessary. Using a closure, you can process the data only once, and maintain a reference to it from within the source function. In addition, the following solution solves the global namespace problem of @Gerbus's solution, and also allows you to play with the array of data once the user has selected something (for example, removing that item from the list).

  // Setup the auto-complete box of users
  var setupUserAcUi = function(data) {
      var objects = [];
      var map = {};
      $.each(data, function(i, object) {
          map[object.name] = object;
          objects.push(object.name);
      });

      // The declaration of the source and updater functions, and the fact they
      // are referencing variables outside their scope, creates a closure
      $("#splitter-findusers").typeahead({
        source: function(query, process) {
            process(objects);
        },
        updater: function(item) {
            var mapItem = map[item];
            objects.splice( $.inArray(item, objects), 1 ); // Remove from list
            // Perform any other actions
        }
      });
  };

  // `data` can be an array that you define,
  // or you could pass `setupUserAcUi` as the callback to a jQuery.ajax() call
  // (which is actually how I am using it) which returns an array
  setupUserAcUi(data);

The selected answer is a bit of a hack. I was looking for the same thing, and this approach works beautifully:

https://github.com/twbs/bootstrap/pull/3682

It keeps two arrays, one for the name that typeahead shows, and one for the object from which the name is extracted. When one of the options is selected, it uses the name to find the object from whence it came.


Here is an encapsulated solution. This solution allows you have more than one typeahead on the same page.

This is a modified version of #13279176 Gerbus answer.

$('.make-me-typeahead').typeahead({
    source: function (query) {
        var self = this;
        self.map = {};
        var items = [];

        var data = [
            {"id": 1, "label": "machin"},
            {"id": 2, "label": "truc"}
        ];

        $.each(data, function (i, item) {
            self.map[item.label] = item;
            items.push(item.label)
        });

        return items;
    },

    updater: function (item) {
        var selectedItem = this.map[item];
        this.$element.data('selected', selectedItem);
        return item;
    }
});

Now when you need get the key of the current selected item you just need do $('.make-me-typeahead').data('selected')


I've been struggling with this problem myself, here is the solution I came up with, for data of the type:

[{'id':an_id, 'name':a_name}]

Was:

$("#memberSearch").typeahead({
            source: function (query, process) {
                var $this = this //get a reference to the typeahead object
                return $.get('/getSwimmerListJSON',function(data){
                    var options = [];
                    $this["map"] = {}; //replace any existing map attr with an empty object
                    $.each(data,function (i,val){
                        options.push(val.name);
                        $this.map[val.name] = val.id; //keep reference from name -> id
                    });
                    return process(options);
                });
            },
            updater: function (item) {
                console.log(this.map[item],item); //access it here

            }

        });

Just another way to implement Pierref function.

var separator = "####";
$("'.autocomplete'").typeahead({
    minLength: 3,
    source: function (query, process) {
        var config = {
            type: 'POST',
            url: 'Requests/AJAX.PHP', //Change it
            cache: 'false',
            data: {
                query: query
            },
            dataType: 'json'
        };

        config.beforeSend = function () {
            //TODO : loading gif
        };

        config.error = function (json) {
            if (json.error) {
                alert(json.error);
            }
        };

        config.success = function (json) {
            if (json.error) {
                alert(json.error);
            }
            var data = [];
            for (var i = 0; i < json.data.length; i++) {
                data.push(json.data[i].id + separator + json.data[i].name);
            }

            process(data);
        };

        $.ajax(config);
    },
    highlighter: function (item) {
        var parts = item.split(separator);
        parts.shift();
        return parts.join(separator);
    },
    updater: function (item) {
        var parts = item.split(separator);
        $('.autocomplete').val(parts.shift());
        return parts.join(separador);
    }
});

The selected answer doesn't deal with non unique labels (e.g. a person's name). I'm using the following which keeps the default highlighter formatting:

            var callback = function(id) {
                console.log(id);
            };

            $('.typeahead',this.el).typeahead({
                source: function (query, process) {

                    var sourceData = [
                        {id:"abc",label:"Option 1"},
                        {id:"hfv",label:"Option 2"},
                        {id:"jkf",label:"Option 3"},
                        {id:"ds",label:"Option 4"},
                        {id:"dsfd",label:"Option 5"},
                    ];

                    var concatSourceData = _.map(sourceData,function(item){
                        return item.id + "|" + item.label;
                    });

                    process(concatSourceData);
                },

                matcher : function(item) {
                    return this.__proto__.matcher.call(this,item.split("|")[1]);
                },

                highlighter: function(item) {
                    return this.__proto__.highlighter.call(this,item.split("|")[1]);
                },

                updater: function(item) {
                    var itemArray = item.split("|");
                    callback(itemArray[0]);
                    return this.__proto__.updater.call(this,itemArray[1]);
                }
            });

I made an Angular 2 directive, typeahead-angular2, that does exactly what you want, and handles the case of non-unique labels as well. You can take the typeahead part.

This directive handle complexe objects with multiple attributes and handle the case where the label isn't unique. it basicly recieves 4 parameters :

  • @Input() name; //name for typeahead
  • @Input() objectsDataSet; // a dataSet of objects , it could be any kind of object
  • @Input() handleFunction; // a callback function that is called when the object is selected , you can pass the object or whatever you want to this function.
  • @Input() labelAtt; // the label attribute (object[labelAtt] is displayed to the user , it must be a string).

example :

<input type="text" class="form-control" placeholder="Name..." typeaheadautocomplete [objectsDataSet]="clientList" [labelAtt]="'Firstname'" [name]="'clients'" [handleFunction]="logClient">

as you can see : clientList is an array of "client" objects , let's say {"Fistname":"Billel","Lastname":"Guerfa",....} we use the Firstname attribute for the autocomplete list. logClient here recieves a client object and displays it.

Dependencies :

just declare the typeahead script at the index.html level.

See: https://github.com/BillelGuerfa/typeahead-angular2/


Need Your Help

Can't get the most simple knockout.js sample to work?

javascript html5 knockout.js

This really bothers me. Please have a look at the Hello World example of knockout.js.

List files not matching a pattern?

bash wildcard glob ls

Here's how one might list all files matching a pattern in bash: