I've opened up an ExtJs project that I've not had my head in for some time, and this is baffling me.
I've an Ext.form.ComboBox that uses a remote JSON store to list users. I use an XTemplate to format the users as listed in the drop down:
'<tpl for="."><div class="x-combo-list-item">',
'{firstname} {lastname} ({email})',
'</div></tpl>'
When I expand the drop down, I see my users listed correctly:
John Smith ([email protected])
John Ford ([email protected])
However when I click on a user, the combo box contents change to the valueField property ('firstname') which you would expect.
Issues:
Instead of showing John, I'd like the combo box to show: John Smith ([email protected]).
When I have two John's (John Smith and John Ford) and the form loads, the ExtJs logic matches against the first John it finds in the list and changes the value of the field to the first John it matches against.
For example: John Smith (ID = 1) John Ford (ID = 2)
The user chooses John Ford, and "John" appears in the combo box after they've clicked the combo menu item, and user_id = 2 is written to the database.
When I reload the page however, the name "John" is matched (loaded from the database) to the first list entry, and if the operator does not manually change the selection in the drop down dialog, then John Smith is selected and user_id = 1 is now written to the database (when the user saves the form).
Any input would be much appreciated. My gut tells me there should be a couple of hooks during load and post list click that will allow me to manipulate what is written to the innerHTML element of the element.
~~~~~~~~~~~~~
Note: I've inheriting from a custom class which allows me to type ahead query against, firstname, lastname and email address (as we may potentially have hundreds of users to search against).
The ComboBox element I'm inheriting from:
CW.form.CustomComboBox = Ext.extend( Ext.form.ComboBox, {
filterKeys:[],
// Note: This overrides the standard doQuery function in Ext 3.3
doQuery: function(q, forceAll){
q = Ext.isEmpty(q) ? '' : q;
var qe = {
query: q,
forceAll: forceAll,
combo: this,
cancel:false
};
if(this.fireEvent('beforequery', qe)===false || qe.cancel){
return false;
}
q = qe.query;
forceAll = qe.forceAll;
if(forceAll === true || (q.length >= this.minChars)){
if(this.lastQuery !== q){
this.lastQuery = q;
if(this.mode == 'local'){
this.selectedIndex = -1;
if(forceAll){
this.store.clearFilter();
}else{
// this.store.filter(this.displayField, q);
this.store.filterBy( function(rec,id){
return this.filterFn(rec,id,q);
}, this );
}
this.onLoad();
}else{
this.store.baseParams[this.queryParam] = q;
this.store.load({
params: this.getParams(q)
});
this.expand();
}
}else{
this.selectedIndex = -1;
this.onLoad();
}
}
},
/**
* Custom function for filtering the store
*/
filterFn: function(rec, id, q ){
// var filterKeys = ['id', 'firstname', 'lastname', 'email'];
var parts = q.split(' ');
// If no filter applied then show no results
if(parts.length == 0){
return false;
}
// Iterate through each of the parts of the user string
// They must all match, at least in part, one of the filterKeys
// (i.e. id, email, firstname, etc.)
for(i=0; i<parts.length;i++){
var foundPart = false;
// Create a RegExp object for this search snippet (i.e. '@gmai')
var matcher = this.store.data.createValueMatcher(parts[i] , true);
// Search until this matches one of the keys for this record
for(j=0;j<this.filterKeys.length; j++){
if(matcher.test(rec.get(this.filterKeys[j]))){
foundPart = true;
break;
}
}
// If there are no fields of the record matching this part,
// the record does not match (return false)
if( foundPart == false ){
return false;
}
}
return true;
},
initComponent: function(){
Ext.applyIf(this,{
listeners:{
beforequery: function(qe){
delete qe.combo.lastQuery;
return true;
}
}
});
if(this.filterKeys.length == 0){
this.filterKeys = [this.displayField];
}
CW.form.CustomComboBox.superclass.initComponent.call(this);
}
});
Ext.reg('custom-combo', CW.form.CustomComboBox);
Regarding issue #1, the best way I have found to get good custom display fields is to use generated fields in the Ext.data.Record definition that the Store uses. That way you get the full record to access to create your display field and aren't limited to just one field. I can't find the 3.x examples online now that Sencha is moving on to Ext4, but you can find this example in the examples/form
directory of your ExtJS download. Here I've modified one of the ExtJS combo examples (examples/form/combo.js
):
var store = new Ext.data.ArrayStore({
fields: ['abbr', 'state', 'nick', {
name: 'display',
convert: function(v, rec) { return rec[1] +' - '+ rec[0] }
// display looks like 'Texas - TX'
}],
data : Ext.exampledata.states // from states.js
});
var combo = new Ext.form.ComboBox({
store: store,
displayField:'display',
typeAhead: true,
mode: 'local',
forceSelection: true,
triggerAction: 'all',
emptyText:'Select a state...',
selectOnFocus:true,
applyTo: 'local-states'
});
And now the combo displays values like Texas - TX
, or whatever you have convert
output. You can find documentation for convert
in the Ext.data.Field docs.
As for issue #2, you may need to set the idProperty
for your Ext.data.Reader or your store if you're using one of the convenient store + reader combos like JsonStore or ArrayStore. idProperty
tells Ext which field to look for for a unique identifier. You can get all kinds of weird behavior if you don't have an idProperty
or you choose one that isn't unique. Docs for that are here.