How to implement "mustMatch" and "selectFirst" in jQuery UI Autocomplete?
Asked Answered
B

13

38

I recently migrated a few of my Autocomplete plugins from the one produced by bassistance to the jQuery UI autocomplete.

How can the "mustMatch" and "selectFirst" be implemented with just callbacks and other options without modifying the core autocomplete code itself?

Brade answered 6/4, 2010 at 18:40 Comment(0)
B
40

I think I solved both features...

To make things easier, I used a common custom selector:

$.expr[':'].textEquals = function (a, i, m) {
    return $(a).text().match("^" + m[3] + "$");
};

The rest of the code:

$(function () {
    $("#tags").autocomplete({
        source: '/get_my_data/',
        change: function (event, ui) {
            //if the value of the textbox does not match a suggestion, clear its value
            if ($(".ui-autocomplete li:textEquals('" + $(this).val() + "')").size() == 0) {
                $(this).val('');
            }
        }
    }).live('keydown', function (e) {
        var keyCode = e.keyCode || e.which;
        //if TAB or RETURN is pressed and the text in the textbox does not match a suggestion, set the value of the textbox to the text of the first suggestion
        if((keyCode == 9 || keyCode == 13) && ($(".ui-autocomplete li:textEquals('" + $(this).val() + "')").size() == 0)) {
            $(this).val($(".ui-autocomplete li:visible:first").text());
        }
    });
});

If any of your autocomplete suggestions contain any 'special' characters used by regexp, you must escape those characters within m[3] in the custom selector:

function escape_regexp(text) {
  return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}

and change the custom selector:

$.expr[':'].textEquals = function (a, i, m) {
  return $(a).text().match("^" + escape_regexp(m[3]) + "$");
};
Brade answered 11/4, 2010 at 4:10 Comment(9)
Were you also able to get "autoFill" working? I've got a question here #2934213 but I can't for the life of me get autofill to work.Ruthenian
How could I customize this to use the ui object that is passed along side the event parameter in focus, select, etc of the plugin?Clabo
I almost got this to work (just the mustmatch stuff) but it appears to clear the value regardless of whether it was suggested or not. Any ideas? :/Whereat
Update: I did a bit of debugging, it appears as though the expression is called for every single suggested autocomplete value, and since the last in the list of suggestions doesn't match the selected value it returns false and empties the selected value from the input box. Any ideas why it's iterating through all of them? Maybe I've messed something up!Whereat
Update2: Looks like the regexp is returning null even when things are the same, so perhaps an issue there. Thinking it might be because my string actually has square brackets in which might be screwing up the regexp.Whereat
Update3: Yes, it was regexp characters messing it up - have edited the answer to suggest this in case anyone else bumps into that problem.Whereat
Thank you! I have been looking for a good solution! I have one question.. I'm using Geocoding to populate the suggestion list. In the city input I'm adding just the city. The suggestion has the city, state, and country so naturally it wont match the exact input value. Do you know how yo match just the cities?Polyanthus
I believe this fails when more than one autocomplete on the same page might have matching elements (since your selector is global). Recommend: var list = $(this).data("autocomplete").menu.element[0]; and then match on $(list).children(...) . Trying to test now.Jujitsu
this solution does not work if you have an autocomplete with multiple values . . any suggestions?Conceptualism
P
36

I used something as simple as this for mustMatch and it works. I hope it helps someone.

        change: function (event, ui) {
            if (!ui.item) {
                 $(this).val('');
             }
        }
Peonir answered 18/8, 2011 at 20:49 Comment(2)
Perfect, thanks! Have you implemented autofill? Looking for that too :)Suppose
Just to note that you should make sure you have autoFocus: trueSignory
C
3

I think I got the mustMatch working with this code... It needs thorough test though:

<script type="text/javascript">
    $(function() {
        $("#my_input_id").autocomplete({
            source: '/get_my_data/',
            minChars: 3,
            change: function(event, ui) {
                // provide must match checking if what is in the input
                // is in the list of results. HACK!
                var source = $(this).val();
                var found = $('.ui-autocomplete li').text().search(source);
                console.debug('found:' + found);
                if(found < 0) {
                    $(this).val('');
                }
            }
        });
    });
</script>
Carper answered 10/4, 2010 at 11:9 Comment(1)
Good but I can submit an empty value on return. I think that you have to implement a keydown function.Polyanthus
A
2

I found this question to be useful.

I thought I'd post up the code I'm now using (adapted from Esteban Feldman's answer).

I've added my own mustMatch option, and a CSS class to highlight the issue before resetting the textbox value.

       change: function (event, ui) {
          if (options.mustMatch) {
            var found = $('.ui-autocomplete li').text().search($(this).val());

            if (found < 0) {
              $(this).addClass('ui-autocomplete-nomatch').val('');
              $(this).delay(1500).removeClass('ui-autocomplete-nomatch', 500);
            }
          }
        }

CSS

.ui-autocomplete-nomatch { background: white url('../Images/AutocompleteError.gif') right center no-repeat; }
Armourer answered 27/4, 2010 at 11:57 Comment(1)
@GordonB - do you have the images anywhere autocompleteerror.gifConceptualism
H
1

The solution I've used to implement 'mustMatch':

<script type="text/javascript">
...

$('#recipient_name').autocomplete({
    source: friends,
    change: function (event, ui) {
        if ($('#message_recipient_id').attr('rel') != $(this).val()) {
            $(this).val('');
            $('#message_recipient_id').val('');
            $('#message_recipient_id').attr('rel', '');
        }
    },
    select: function(event, ui) {
        $('#message_recipient_id').val(ui.item.user_id);
        $('#message_recipient_id').attr('rel', ui.item.label);
    }
}); 

...
</script>
Hallucination answered 11/5, 2011 at 16:18 Comment(0)
P
1

I discovered one issue. While the suggestion list is active you can submit your form even if the value doesn't match the suggestion. To dissallow this I added:

$('form').submit(function() {
        if ($(".ui-autocomplete li:textEquals('" + $(this).val() + "')").size() == 0) {
            $(this).val('');
            $("span").text("Select a valid city").show();
            return false;
        }
});

This prevents the form from being submitted and displays a message.

Polyanthus answered 9/6, 2011 at 23:17 Comment(1)
Note: this appears to be a comment on the Doc Hoffiday accepted answer.Fool
F
1

This JQuery-UI official demo has mustMatch, amongst other cool stuff: http://jqueryui.com/demos/autocomplete/#combobox

I've updated it to add autoFill, and a few other things.

Javascript:



/* stolen from http://jqueryui.com/demos/autocomplete/#combobox
 *
 * and these options added.
 *
 * - autoFill (default: true):  select first value rather than clearing if there's a match
 *
 * - clearButton (default: true): add a "clear" button
 *
 * - adjustWidth (default: true): if true, will set the autocomplete width the same as
 *    the old select.  (requires jQuery 1.4.4 to work on IE8)
 *
 * - uiStyle (default: false): if true, will add classes so that the autocomplete input
 *    takes a jQuery-UI style
 */
(function( $ ) {
    $.widget( "ui.combobox", {
        options: {
            autoFill: true,
            clearButton: true,
            adjustWidth: true,
            uiStyle: false,
            selected: null,
        },
    _create: function() {
        var self = this,
          select = this.element.hide(),
          selected = select.children( ":selected" ),
          value = selected.val() ? selected.text() : "",
              found = false;
        var input = this.input = $( "" )
                .attr('title', '' + select.attr("title") + '')
        .insertAfter( select )
        .val( value )
        .autocomplete({
            delay: 0,
            minLength: 0,
            source: function( request, response ) {
                var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" );
                        var resp = select.children( "option" ).map(function() {
                    var text = $( this ).text();
                    if ( this.value && ( !request.term || matcher.test(text) ) )
                    return {
                        label: text.replace(
                        new RegExp(
                            "(?![^&;]+;)(?!]*)(" +
                            $.ui.autocomplete.escapeRegex(request.term) +
                            ")(?![^]*>)(?![^&;]+;)", "gi"
                        ), "$1" ),
                        value: text,
                        option: this
                    };
                });
                        found = resp.length > 0;
                response( resp );
            },
            select: function( event, ui ) {
                ui.item.option.selected = true;
                self._trigger( "selected", event, {
                    item: ui.item.option
                });
            },
            change: function( event, ui ) {
                if ( !ui.item ) {
                    var matcher = new RegExp( "^" + $.ui.autocomplete.escapeRegex( $(this).val() ) + "$", "i" ),
                    valid = false;
                    select.children( "option" ).each(function() {
                    if ( $( this ).text().match( matcher ) ) {
                        this.selected = valid = true;
                        return false;
                    }
                    });
                    if ( !valid || input.data("autocomplete").term=="" ) {
                    // set to first suggestion, unless blank or autoFill is turned off
                                var suggestion;
                                if(!self.options.autoFill || input.data("autocomplete").term=="") found=false;
                                if(found) {
                                    suggestion = jQuery(input.data("autocomplete").widget()).find("li:first")[0];
                                    var option = select.find("option[text="+suggestion.innerText+"]").attr('selected', true);
                                    $(this).val(suggestion.innerText);
                        input.data("autocomplete").term = suggestion.innerText;
                            self._trigger( "selected", event, { item: option[0] });
                                } else {
                                    suggestion={innerText: ''};
                                    select.find("option:selected").removeAttr("selected");
                                    $(this).val('');
                        input.data( "autocomplete" ).term = '';
                                    self._trigger( "selected", event, { item: null });
                                }
                    return found;
                    }
                }
            }
        });

            if( self.options.adjustWidth ) { input.width(select.width()); }

            if( self.options.uiStyle ) {
                input.addClass( "ui-widget ui-widget-content ui-corner-left" );
            }


        input.data( "autocomplete" )._renderItem = function( ul, item ) {
            return $( "
  • " ) .data( "item.autocomplete", item ) .append( "" + item.label + "" ) .appendTo( ul ); }; this.button = $( " " ) .attr( "tabIndex", -1 ) .attr( "title", "Show All Items" ) .insertAfter( input ) .button({ icons: { primary: "ui-icon-triangle-1-s" }, text: false }) .removeClass( "ui-corner-all" ) .addClass( "ui-corner-right ui-button-icon" ) .click(function() { // close if already visible if ( input.autocomplete( "widget" ).is( ":visible" ) ) { input.autocomplete( "close" ); return; } // work around a bug (likely same cause as #5265) $( this ).blur(); // pass empty string as value to search for, displaying all results input.autocomplete( "search", "" ); input.focus(); }); if( self.options.clearButton ) { this.clear_button = $( " " ) .attr( "tabIndex", -1 ) .attr( "title", "Clear Entry" ) .insertAfter( input ) .button({ icons: { primary: "ui-icon-close" }, text: false }) .removeClass( "ui-corner-all" ) .click(function(event, ui) { select.find("option:selected").removeAttr("selected"); input.val( "" ); input.data( "autocomplete" ).term = ""; self._trigger( "selected", event, { item: null }); // work around a bug (likely same cause as #5265) $( this ).blur(); }); } }, destroy: function() { this.input.remove(); this.button.remove(); this.element.show(); $.Widget.prototype.destroy.call( this ); } }); })( jQuery );

    CSS (.hjq-combobox is a wrapping span)

    .hjq-combobox .ui-button { margin-left: -1px; }
    .hjq-combobox .ui-button-icon-only .ui-button-text { padding: 0; }
    .hjq-combobox button.ui-button-icon-only { width: 20px; }
    .hjq-combobox .ui-autocomplete-input { margin-right: 0; }
    .hjq-combobox {white-space: nowrap;}
    

    Note: this code is being updated and maintained here: https://github.com/tablatom/hobo/blob/master/hobo_jquery_ui/vendor/assets/javascripts/combobox.js

    Fool answered 17/6, 2011 at 15:4 Comment(0)
    B
    1

    Maybe it's just because this is an old issue, but I found that the easiest solution is already there in the plugin, you just need to use the proper functions to access it.

    This code will handle the cases when the autocomplete loses focus with an invalid value:

    change: function(e, ui) {
        if (!ui.item) {
            $(this).val("");
        }
    }
    

    And this code, much like the original functionality from bassistance, will handle the cases when there are no matches while typing in the autocomplete:

    response: function(e, ui) {
        if (ui.content.length == 0) {
            $(this).val("");
        }
    }
    

    This works well with either a static array source, or a JSON data source. Combined with the autoFocus: true option, it seems to do everything needed in an efficient manner.

    The last case that you may want to handle is what to do when the ESCAPE key is pressed with an invalid value in the textbox. What I do is use the value of the first matched result. And this is how I do that...

    First, declare a variable to hold the best match. Do this outside of your autocomplete plugin.

    var bestMatch = "";
    

    Then use the following option:

    open: function(e, ui) {
        bestMatch = "";
    
        var acData = $(this).data('uiAutocomplete');
        acData.menu.element.find("A").each(function () {
            var me = $(this);
    
            if (me.parent().index() == 0) {
                bestMatch = me.text();
            }
        });
    }
    

    Lastly, add the following event to your autocomplete:

    .on("keydown", function(e) {
        if (e.keyCode == 27)        // ESCAPE key
        {
            $(this).val(bestMatch);
        }
    })
    

    You can just as easily force the field to be empty when the escape key is pressed. All you have to do is set the value to an empty string when the key is pressed instead of the bestMatch variable (which isn't needed at all if you choose to empty the field).

    Botticelli answered 27/6, 2014 at 18:13 Comment(0)
    Y
    0

    I'm doing it a little differently, caching the results and clearing the text field if the number of results for a certain term is zero:

    <script type='text/javascript'>
    function init_autocomplete( args )
    {
         var resultCache = {};
         var currentRequestTerm = null;
    
         var closeCallback = function()
         {
             // Clear text field if current request has no results
             if( resultCache[currentRequestTerm].length == 0 )
                 $(args.selector).val('');
         };
    
         var sourceCallback = function( request, responseCallback )
         {
             // Save request term
             currentRequestTerm = request.term;
    
             // Check for cache hit
             // ...
             // If no cache hit, fetch remote data
             $.post(
                 dataSourceUrl,
                 { ...  }, // post data
                 function( response )
                 {
                     // Store cache
                     resultCache[request.term] = response;
    
                     responseCallback( response );
                 }
         };
    
         $(args.selector).autocomplete({
             close:  closeCallback,
             source: sourceCallback
         });
    }
    </script>
    
    Yeseniayeshiva answered 1/7, 2010 at 21:59 Comment(0)
    C
    0

    Scott Gonzalez has written a selectFirst extension (as well as several others) for jQueryUI AutoComplete.

    Celanese answered 21/12, 2011 at 21:45 Comment(0)
    S
    0

    Based on the accepted answer:

    My additional requirements: multiple autocompletes, unobtrusive error validation.

    change: function () {
        var target = $(this),
            widget = target.autocomplete('widget'),
            cond = widget.find('li:textEquals("' + target.val() + '")').length === 0;
    
        target.toggleClass('input-validation-error', cond);
    }
    
    Springclean answered 24/9, 2014 at 14:36 Comment(0)
    A
    0

    Late reply but might help someone!

    Considering the two events in autocomplete widget

    1) change - triggered when field is blurred and value is changed.

    2) response - triggered when the search completes and the menu is shown.

    Modify the change and response events as follows:

    change : function(event,ui)
    {  
    if(!ui.item){
    $("selector").val("");
    }
    },
    
    response : function(event,ui){
    if(ui.content.length==0){
      $("selector").val("");
    }
    }
    

    Hope this helps!

    Animalcule answered 8/12, 2015 at 5:56 Comment(0)
    F
    0

    Here a simple definitive solution for "mustMatch" requirement:

    <script type="text/javascript">
        $(function() {
            $("#my_input_id").autocomplete({
                source: '/get_my_data/',
                minChars: 3,
                select: function(event, ui) {
                    // custom code
                    $(this).data("pre-ui-autocomplete-value", $(this).val());
                }
            }).on("focus", function () {
                $(this).data("pre-ui-autocomplete-value", $(this).val());
            }).on("blur", function () {
                $(this).val($(this).data("pre-ui-autocomplete-value"));
            });
        });
    </script>
    
    Flout answered 27/3, 2020 at 15:21 Comment(0)

    © 2022 - 2024 — McMap. All rights reserved.