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

20,857

Solution 1

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()
}

Solution 2

A somewhat simpler solution to the problem is to include the following in your click event:

var typeahead = $(this).data("typeahead");
if (typeahead && this.value != typeahead.$menu.find('.active').attr('data-value')) 
  return;

The first line will retrieve the typeahead if it is attached. The second will assure that the current value matches the selected value.

You can also do a check for typeahead.mousedover within the if to allow invalid (meaning not a typeahead option) input to still trigger the default change behavior when the control loses focus without making a selection.

Share:
20,857
Dan Walmsley
Author by

Dan Walmsley

Updated on October 04, 2020

Comments

  • Dan Walmsley
    Dan Walmsley over 3 years

    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?

  • Daniel
    Daniel almost 10 years
    Nice trial and error. However, you do not need to override the select function if you update your check to something like this: if (typeahead.mousedover && typeahead.shown) mousedover is not updated to false when the typeover menu is closed... but shown is updated.