How to get Bootstrap typeahead click result before the input change event

Dan Walmsley picture Dan Walmsley · Apr 3, 2013 · Viewed 19.6k times · Source

I am trying to use bootstraps typeahead as a flexible select for users. So if the item is known they select it if not a dialogue opens up allowing them to enter a new item.

I am doing this by watching for the change event on the input, if the inputs val is not in the source array (for the the typeahead) then the "add item" dialogue is shown to the user. The problem is if the user clicks one of the options a change event is sent before the typeahead has a chance to set the val. This is as the click will cause a blur on the text.

I wanted to check the active element to get around this, so in the change event look at document.activeElement and see if it is one of the typeahead options, this did not work and returned the entire body element.

Here is a trimmed down version of the code:

Html

<div id="contactModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
  <div class="modal-header">
    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
    <h3 id="myModalLabel">Unknown Contact</h3>
  </div>
  <div class="modal-body">
    <p>The contact you have entered is unknown. Press cancel to enter a different contact or fill out the details below to create a new contact</p>
    <label>Name</label>
    <input id="modal_contact"  type="text" placeholder="dealership contact" value=""/>
    <label>Email</label>
    <input id="modal_contact_email" type="text" placeholder="dealership contact" value=""/>
  </div>
  <div class="modal-footer">
    <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
    <button id="save" class="btn btn-primary">Save changes</button>
  </div>
</div>

Javascript

var contacts = ['pete','geoff'];

$('.typeahead').typeahead({source:contacts, updater : function(item){
    console.log('updater fired'); //fires after first change event
    return item;
}});

$('input').change(function(ev){
    console.log($(ev.target).val());

    if ($.inArray($(ev.target).val(), contacts) < 0)
        $('#contactModal').modal();
})

And a JSFiddle version http://jsfiddle.net/k39vM/

Does any one know how I can test if the user has clicked a typeahead to find out if that caused the change event?

Answer

pickypg picture pickypg · Apr 19, 2013

My previous suggestion didn't actually work because, as you mentioned, there is a somewhat unexpected blur event triggering the change before event the click on the menu. I had assumed that it was related to the this.hide() call inside the Typeahead.prototype.select method.

However, after a bit of trial-and-error, I do think that I may have found a workaround. It's not actually the this.hide() call within the select method that is causing the problem. Knowing that there are two ways that the user can trigger selection helps to understand the hopefully working workaround (I only tested in Chrome): using the keyboard, such as hitting enter, or clicking on the item. As a result, knowing that the click is the problem-child of the two, I noticed that the mousedover event is maintained when the user mouses over the dropdown.

As a result, the odd behavior can be manually ignored within a change event. To simplify the process of determining what actually causes the next change event I used a different (custom) event called "selected" to denote the user has changed the value rather than a standard change. Unfortunately, you still must manually ignore change events when the mousedover property is true:

Simplified HTML (I used the p tag because I find that Chrome has trouble with debugging JSFiddle's JavaScript, and combining with the console lead to a bad time):

<input class="typeahead " type="text" placeholder="contact" value="Peter skillet"></input>
<input type="text"></input>
<p id="text" />

JavaScript (text can be replaced by console.log for the those interested comfortable):

var contacts = ['pete', 'geoffrey'];

var $text = $("#text");
var typeahead = $('.typeahead').typeahead({source: contacts}).data('typeahead');

typeahead.select = function () {
    var val = this.$menu.find('.active').attr('data-value');
    this.$element.val(this.updater(val))
        .trigger('selected'); // <- unique event
    return this.hide()
};

$('input.typeahead').on('change', function (e) {
    var $target = $(e.target);
    if (typeahead.mousedover) {
        $text.html($text.html() + "mousedover [ignored selection]<br />");
    }
    else if ($.inArray($target.val(), contacts) < 0) {
        $text.html($text.html() + "show add<br/>");
    }
}).on('selected', function () {
    $text.html($text.html() + "selected<br />");
});

For reference, this is the default select method:

select: function () {
  var val = this.$menu.find('.active').attr('data-value')
  this.$element
    .val(this.updater(val))
    .change()
  return this.hide()
}