Question

When a combobox is used in ExtJS (tested in 4.2, but likely other versions as well), and the "typeAhead: true" option is set, if you the user types in values very quickly and then hits the "tab" on their keyboard, the focus will move to the next field on the screen and the wrong value is set. Because of the tricky nature of this bug, I have created a JSFiddle for it here: http://jsfiddle.net/59AVC/2/

To replicate the bug, very quickly type "975" and then "tab" in the first combobox field. If you hit tab very quickly after you enter the "5" in "975", you will see that the combobox is set to the "970" option instead. I believe this is happening because the "Tab" is causing whatever option is highlighted in the list to be the value that is set, but what is strange is that the "970" is highlighted still after the "5" in "975" is entered, when it should process that event first before the "tab" and it should have changed the selection to be the correct "975".

I have tried adjusting the typeAheadDelay (set to 0 in the example), as well as the queryDelay and everything else I can think of. It looks like ExtJS is simply canceling the lookup that is somehow still running and not finished when the tab is pressed.

Any suggestions on how to work around this bug? Do I need to write my own "typeAhead" auto-complete function to handle this correctly by single threading the events?

Here is the sample JSFiddle code that shows this:

// The data store containing the list of values
var states = Ext.create('Ext.data.Store', {
    fields: ['val_number', 'val_name'],
    data : [
        {"val_number":"970", "val_name":"970 - Name"},
        {"val_number":"971", "val_name":"971 - Name"},
        {"val_number":"972", "val_name":"972 - Name"},
        {"val_number":"973", "val_name":"973 - Name"},
        {"val_number":"974", "val_name":"974 - Name"},
        {"val_number":"975", "val_name":"975 - Name"}
        //...
    ]
});

Ext.create('Ext.form.ComboBox', {
    fieldLabel: 'Choose 1st Value',
    store: states,
    queryMode: 'local',
    displayField: 'val_name',
    valueField: 'val_number',
    renderTo: Ext.getBody(),
    typeAhead: true,
    typeAheadDelay: 0,
    minChars: 1,
    forceSelection: true,
    autoSelect: false,
    triggerAction: 'all',
    queryDelay: 0,
    queryCaching: false
});


Ext.create('Ext.form.ComboBox', {
    fieldLabel: 'Choose 2nd Value',
    store: states,
    queryMode: 'local',
    displayField: 'val_name',
    valueField: 'val_number',
    renderTo: Ext.getBody(),
    typeAhead: true,
    typeAheadDelay: 0,
    minChars: 1,
    forceSelection: true,
    autoSelect: false,
    triggerAction: 'all',
    queryDelay: 0,
    queryCaching: false
});

UPDATED: Tried this code as suggested, no change in result - still doesn't select correctly:

Ext.define('App.CustomComboBox', {
    extend: 'Ext.form.field.ComboBox',
    alias: 'widget.CustomCombobox',
    initComponent:function() {
        // call parent init component
        this.callParent(arguments);
    },
    onTypeAhead: function() {
        console.log('onTypeAhead...');
        var me = this,
            displayField = me.displayField,
            record = me.store.findRecord(displayField, me.getRawValue()),
            boundList = me.getPicker(),
            newValue, len, selStart;

        if (record) {
            newValue = record.get(displayField);
            len = newValue.length;
            selStart = me.getRawValue().length;

            //boundList.highlightItem(boundList.getNode(record));

            if (selStart !== 0 && selStart !== len) {
                me.setRawValue(newValue);
                me.selectText(selStart, newValue.length);
            }
        }
    }
});
Was it helpful?

Solution 2

Thanks to Jandalf, I have some good news. I was able to work out a solution for my needs by extending the combobox and introducing a few fixes. The first was to do as Jandalf suggested (a good starting point) and the next set of fixes was to stop using a DelayedTask if the delay was 0 or less (my config sets "typeAheadDelay" and "queryDelay" to 0). Finally, I had to also do a check in the "assertValue" that is the equivalent of what happens when someone types a regular key to catch the problem where the tab is blurring things before the keyUp is done. Because of this last part, it may not be the perfect solution for everyone, but it was the only thing that could solve my problem. So, here is the code that makes it work for me. I hope someone else will find it useful.

Ext.define('App.CustomComboBox', {
    extend: 'Ext.form.field.ComboBox',
    alias: 'widget.CustomCombobox',
    initComponent:function() {
        // call parent init component
        this.callParent(arguments);
    },
    onTypeAhead: function() {
        var me = this,
            displayField = me.displayField,
            record = me.store.findRecord(displayField, me.getRawValue()),
            boundList = me.getPicker(),
            newValue, len, selStart;

        if (record) {
            newValue = record.get(displayField);
            len = newValue.length;
            selStart = me.getRawValue().length;

            //Removed to prevent onBlur/Tab causing invalid selections
            //boundList.highlightItem(boundList.getNode(record));

            if (selStart !== 0 && selStart !== len) {
                me.setRawValue(newValue);
                me.selectText(selStart, newValue.length);
            }
        }
    },

    onPaste: function(){
        var me = this;

        if (!me.readOnly && !me.disabled && me.editable) {
            if (me.queryDelay > 0) {
                //Delay it
                me.doQueryTask.delay(me.queryDelay);
            } else {
                //Changed to do immediately instead of in the delayed task
                me.doRawQuery();
            }
        }
    },

    // store the last key and doQuery if relevant
    onKeyUp: function(e, t) {
        var me = this,
            key = e.getKey();

        if (!me.readOnly && !me.disabled && me.editable) {
            me.lastKey = key;
            // we put this in a task so that we can cancel it if a user is
            // in and out before the queryDelay elapses

            // perform query w/ any normal key or backspace or delete
            if (!e.isSpecialKey() || key == e.BACKSPACE || key == e.DELETE) {
                if (me.queryDelay > 0) {
                    //Delay it
                    me.doQueryTask.delay(me.queryDelay);
                } else {
                    //Changed to do immediately instead of in the delayed task
                    me.doRawQuery();
                }
            }
        }

        if (me.enableKeyEvents) {
            me.callParent(arguments);
        }
    },

    // private
    assertValue: function() {
        var me = this,
            value = me.getRawValue(),
            rec, currentValue;

        if (me.forceSelection) {
            if (me.multiSelect) {
                // For multiselect, check that the current displayed value matches the current
                // selection, if it does not then revert to the most recent selection.
                if (value !== me.getDisplayValue()) {
                    me.setValue(me.lastSelection);
                }
            } else {
                // For single-select, match the displayed value to a record and select it,
                // if it does not match a record then revert to the most recent selection.
                rec = me.findRecordByDisplay(value);
                if (rec) {
                    currentValue = me.value;
                    // Prevent an issue where we have duplicate display values with
                    // different underlying values.
                    if (!me.findRecordByValue(currentValue)) {
                        me.select(rec, true);
                    }
                } else {
                    //Try and query the value to find it as a "catch" for the blur happening before the last keyed value was entered
                    me.doRawQuery();
                    //Get the new value to use
                    value = me.getRawValue();
                    //Copy of the above/same assert value check
                    rec = me.findRecordByDisplay(value);
                    if (rec) {
                        currentValue = me.value;
                        // Prevent an issue where we have duplicate display values with
                        // different underlying values.
                        if (!me.findRecordByValue(currentValue)) {
                            me.select(rec, true);
                        }
                    } else {
                        //This is the original "else" condition
                        me.setValue(me.lastSelection);
                    }
                }
            }
        }
        me.collapse();
    },

    doTypeAhead: function() {
        var me = this;
        if (!me.typeAheadTask) {
            me.typeAheadTask = new Ext.util.DelayedTask(me.onTypeAhead, me);
        }
        if (me.lastKey != Ext.EventObject.BACKSPACE && me.lastKey != Ext.EventObject.DELETE) {
            //Changed to not use the delayed task if 0 or less
            if (me.typeAheadDelay > 0) {
                me.typeAheadTask.delay(me.typeAheadDelay);
            } else {
                me.onTypeAhead();
            }
        }
    }
});

OTHER TIPS

found the problematic code snippet:

beforeBlur: function() {
    this.doQueryTask.cancel();
    this.assertValue();
},

the problem is not typeAhead its selectOnTab together with autoSelect which will be set to true from typeahead.

so this happens:

  1. you type "97" a query will fire and the first value (970) will be selected
  2. you type "5" a query will start, but
  3. you press "tab" and the beforeBlur function gets executed.
  4. the query gets canceled and the currently highlighted value becomes the field value

so what can you do?

  • it is not possible to give the task a callback so assertValue is called after the query finishes :(
  • you need to disable autoselect again and the only way i found is to override onTypeAhead and to comment out the highlighting:

.

onTypeAhead: function() {
    var me = this,
        displayField = me.displayField,
        record = me.store.findRecord(displayField, me.getRawValue()),
        boundList = me.getPicker(),
        newValue, len, selStart;

    if (record) {
        newValue = record.get(displayField);
        len = newValue.length;
        selStart = me.getRawValue().length;

        //boundList.highlightItem(boundList.getNode(record));

        if (selStart !== 0 && selStart !== len) {
            me.setRawValue(newValue);
            me.selectText(selStart, newValue.length);
        }
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top