Success! Looks like we've fixed this one. According to our records the fix was applied for EXTJS-8947 in 4.2.1.744.
  1. #1
    Sencha Premium Member
    Join Date
    Feb 2012
    Location
    Raleigh, NC
    Posts
    445
    Vote Rating
    272
    brian428 has much to be proud of brian428 has much to be proud of brian428 has much to be proud of brian428 has much to be proud of brian428 has much to be proud of brian428 has much to be proud of brian428 has much to be proud of brian428 has much to be proud of brian428 has much to be proud of

      0  

    Default 4.2 ComboBox Query Enhancements

    4.2 ComboBox Query Enhancements


    First, please add config options to combobox for anyMatch and caseSensitive, which are passed to the stored queryFilter.

    Second, please add code to the top of doQuery for:

    Code:
    if( queryString.length < this.minChars ) {
            forceAll = true;
    }
    Because when using field validation (allowBlank: false, forceSelection: true, typeAhead: true), and deleting values typed into the combo box, the full list does not appear (basically forceAll isn't being forced to true).

    Thanks,

    Brian

  2. #2
    Sencha - Ext JS Dev Team Animal's Avatar
    Join Date
    Mar 2007
    Location
    Notts/Redwood City
    Posts
    30,546
    Vote Rating
    64
    Animal is a jewel in the rough Animal is a jewel in the rough Animal is a jewel in the rough

      0  

    Default


    Good ideas. The 4.2.0 release is frozen, but I'll add a ticket.

  3. #3
    Sencha - Ext JS Dev Team Animal's Avatar
    Join Date
    Mar 2007
    Location
    Notts/Redwood City
    Posts
    30,546
    Vote Rating
    64
    Animal is a jewel in the rough Animal is a jewel in the rough Animal is a jewel in the rough

      0  

    Default


    I think what you need to provide the expected user experience is below. Let me know if that works for you.

    Code:
    Ext.override(Ext.form.field.ComboBox, {
        doQuery: function(queryString, forceAll, rawQuery) {
            queryString = queryString || '';
    
            // store in object and pass by reference in 'beforequery'
            // so that client code can modify values.
            var me = this,
                qe = {
                    query: queryString,
                    forceAll: forceAll,
                    combo: me,
                    cancel: false
                },
                store = me.store,
                isLocalMode = me.queryMode === 'local';
    
            if (me.fireEvent('beforequery', qe) === false || qe.cancel) {
                return false;
            }
    
            // get back out possibly modified values
            queryString = qe.query;
            forceAll = qe.forceAll;
    
            // Re-filter if forceAll being passed
            //  or query string is at least the required length
            //  or dropdown is visible and the user will expect feedback - eg erasing and widening the query.
            if (forceAll || (queryString.length >= me.minChars) || (me.picker && me.picker.isVisible())) {
                // expand before starting query so LoadMask can position itself correctly
                me.expand();
    
                // make sure they aren't querying the same thing
                if (!me.queryCaching || me.lastQuery !== queryString) {
                    me.lastQuery = queryString;
    
                    if (isLocalMode) {
    
                        // Querying by a typed string...
                        if (queryString || !forceAll) {
    
                            // Ensure queryFilter is enabled and set the new value
                            me.queryFilter.disabled = false;
                            me.queryFilter.setValue(me.enableRegEx ? new RegExp(queryString) : queryString);
                        }
    
                        // Disable query value filter if no query string or forceAll passed
                        else {
                            me.queryFilter.disabled = true;
                        }
    
                        // Filter the Store according to the updated filter
                        store.filter();
                    } else {
                        // Set flag for onLoad handling to know how the Store was loaded
                        me.rawQuery = rawQuery;
    
                        // In queryMode: 'remote', we assume Store filters are added by the developer as remote filters,
                        // and these are automatically passed as params with every load call, so we do *not* call clearFilter.
                        if (me.pageSize) {
                            // if we're paging, we've changed the query so start at page 1.
                            me.loadPage(1);
                        } else {
                            store.load({
                                params: me.getParams(queryString)
                            });
                        }
                    }
                }
    
                // Clear current selection if it does not match the current value in the field
                if (me.getRawValue() !== me.getDisplayValue()) {
                    me.ignoreSelection++;
                    me.picker.getSelectionModel().deselectAll();
                    me.ignoreSelection--;
                }
    
                if (isLocalMode) {
                    me.doAutoSelect();
                }
                if (me.typeAhead) {
                    me.doTypeAhead();
                }
            }
            return true;
        }
    });

  4. #4
    Sencha Premium Member skirtle's Avatar
    Join Date
    Oct 2010
    Location
    UK
    Posts
    3,625
    Vote Rating
    331
    skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future

      0  

    Default


    My understanding of minChars is that it's to be used with very large, remote datasets where a short query is too small to meaningfully filter the results set. It kind of defeats the purpose if the user can just delete their way around it.

    Could a beforequery listener not be used to achieve the desired effect without any modifications to combobox itself?

  5. #5
    Sencha - Ext JS Dev Team Animal's Avatar
    Join Date
    Mar 2007
    Location
    Notts/Redwood City
    Posts
    30,546
    Vote Rating
    64
    Animal is a jewel in the rough Animal is a jewel in the rough Animal is a jewel in the rough

      0  

    Default


    That's true.

    But for local datasets, it's intuitive that the query widens as you erase.

    Perhaps the fix should be

    Code:
    if (forceAll || (queryString.length >= me.minChars) || (isLocalMode && me.picker && me.picker.isVisible())) {

  6. #6
    Sencha Premium Member
    Join Date
    Feb 2012
    Location
    Raleigh, NC
    Posts
    445
    Vote Rating
    272
    brian428 has much to be proud of brian428 has much to be proud of brian428 has much to be proud of brian428 has much to be proud of brian428 has much to be proud of brian428 has much to be proud of brian428 has much to be proud of brian428 has much to be proud of brian428 has much to be proud of

      0  

    Default


    Quote Originally Posted by Animal View Post
    That's true.

    But for local datasets, it's intuitive that the query widens as you erase.

    Perhaps the fix should be

    Code:
    if (forceAll || (queryString.length >= me.minChars) || (isLocalMode && me.picker && me.picker.isVisible())) {
    Sounds right as a more general solution. For me, I've got a custom AnyMatchComboBox that I use only for local data sets. So for simplicity, I just have this (I'm using CoffeeScript):

    Code:
    config:
      anyMatch: true
      caseSensitive: false
    
    initComponent: ->
      @callParent( [ arguments ] )
      @queryFilter.anyMatch = @anyMatch
      @queryFilter.caseSensitive = @caseSensitive
      
    doQuery: ( queryString="", forceAll, rawQuery ) ->
      forceAll = true if queryString.length < @minChars
      @callParent( queryString, forceAll, rawQuery )
      return true

  7. #7
    Sencha Premium Member skirtle's Avatar
    Join Date
    Oct 2010
    Location
    UK
    Posts
    3,625
    Vote Rating
    331
    skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future skirtle has a brilliant future

      0  

    Default


    I'm not sure it's intuitive for minChars to behave differently for local and remote data. Personally I think it would make more sense for the picker to be automatically hidden if the number of characters drops below minChars, though that would be a change of long-established behaviour.

    In my opinion the main change that is required is to split up the monolith of doQuery to give a few nice template methods for hooking in these kinds of customizations. I've lost count of the number of times that I've seen someone have to copy and paste that whole crazy method just to make a tiny change for their specific use-case.

    A few overridable methods and a bit of documentation and everything would be super-flexible without adding a load of new config options. The documentation here is key, e.g. the existing beforequery event is massively powerful but very few people understand how to use it.

  8. #8
    Sencha - Ext JS Dev Team Animal's Avatar
    Join Date
    Mar 2007
    Location
    Notts/Redwood City
    Posts
    30,546
    Vote Rating
    64
    Animal is a jewel in the rough Animal is a jewel in the rough Animal is a jewel in the rough

      0  

    Default


    Yes, breaking that up so that it could be operated by template methods would be a great solution to this one.

    Configuring the Filter with matchAny and caseSensitive is done. I will look at breaking that method up to allow finer control.

  9. #9
    Sencha - Ext JS Dev Team Animal's Avatar
    Join Date
    Mar 2007
    Location
    Notts/Redwood City
    Posts
    30,546
    Vote Rating
    64
    Animal is a jewel in the rough Animal is a jewel in the rough Animal is a jewel in the rough

      0  

    Default


    How's this for an idea?

    The top of doQuery is simply:

    Code:
            // Decide if, and how we are going to query the store
            queryPlan = me.beforeQuery(queryString, forceAll, rawQuery);
            if (queryPlan === false || queryPlan.cancel) {
                return false;
            }
    After that, it continues with the test for previously cached queryString, and the bifurcation for isLocalMode/non-local.

    That queryPlan is that object containing the queryString, cancel flag, the forceAll flag.

    So the beforeQuery method can be overridden. The base implementation does the default:

    Code:
        beforeQuery: function(queryString, forceAll, rawQuery) {
            var me = this,
                queryPlan = {
                    query: queryString,
                    forceAll: forceAll,
                    combo: me,
                    cancel: false
                };
    
            if (me.fireEvent('beforequery', queryPlan) !== false) {
                if (!queryPlan.cancel && (queryPlan.forceAll || (queryPlan.queryString.length >= me.minChars))) {
                    return queryPlan;
                }
            }
            return false;
        },
    And a bit more documentation, and perhaps some examplage.

  10. #10
    Sencha - Ext JS Dev Team Animal's Avatar
    Join Date
    Mar 2007
    Location
    Notts/Redwood City
    Posts
    30,546
    Vote Rating
    64
    Animal is a jewel in the rough Animal is a jewel in the rough Animal is a jewel in the rough

      0  

    Default


    Actually, better would be

    Code:
        beforeQuery: function(queryString, forceAll, rawQuery) {
            var me = this,
                queryPlan = {
                    query: queryString,
                    forceAll: forceAll,
                    combo: me,
                    cancel: false
                };
    
            // Allow beforequery event to veto by returning false
            if (me.fireEvent('beforequery', queryPlan) == false) {
                queryPlan.cancel = true;
            }
            
            // Allow beforequery event to veto by returning setting the cancel flag
            else if (!queryPlan.cancel) {
                
                // If the minChars threshold has not been met, and we're not forcing an "all" query, cancel the query
                if (queryPlan.query.length < me.minChars && !queryPlan.forceAll) {
                    queryPlan.cancel = true;
                }
            }
            return queryPlan;
        },
    So that override can always rely on a plan returning with the information in it rather than false.

    They may still want to overrule any cancel flag and manipulate the plan.