PDA

View Full Version : Improved remote comboBox



devnull
28 Aug 2008, 2:28 PM
I am sure we are all aware of the issues that arise when using remote comboBoxes in forms and trying to manage a valueField that is different than the displayField. I know I am certainly tired of having to add all sorts of extra code to my forms just to get the combo to display the right value.
So, I whipped up a couple quick overrides that manage loading the store and selecting a value for me automagically when a combo's getValue() function is called (either directly or via various form loading methods). Local combos and remote combos that have already been loaded do not have their behavior changed in any way.
I havent tested it much yet, but it seems to work great!


Ext.override(Ext.data.Store, {
// private
// Keeps track of the load status of the store. Set to true after a successful load event
loaded: false,
/**
* Returns true if the store has previously performed a successful load function.
* @return {Boolean} Whether the store is loaded.
*/
isLoaded: function(){
return this.loaded
},
// private
// Called as a callback by the Reader during a load operation.
loadRecords : function(o, options, success){
if(!o || success === false){
if(success !== false){
this.fireEvent("load", this, [], options);
}
if(options.callback){
options.callback.call(options.scope || this, [], options, false);
}
return;
}
var r = o.records, t = o.totalRecords || r.length;
if(!options || options.add !== true){
if(this.pruneModifiedRecords){
this.modified = [];
}
for(var i = 0, len = r.length; i < len; i++){
r[i].join(this);
}
if(this.snapshot){
this.data = this.snapshot;
delete this.snapshot;
}
this.data.clear();
this.data.addAll(r);
this.totalLength = t;
this.applySort();
this.fireEvent("datachanged", this);
}else{
this.totalLength = Math.max(t, this.data.length+r.length);
this.add(r);
}
this.loaded = true;
this.fireEvent("load", this, r, options);
if(options.callback){
options.callback.call(options.scope || this, r, options, true);
}
}
})

Ext.override(Ext.form.ComboBox,{
/**
* Sets the specified value into the field. If the value finds a match, the corresponding record text
* will be displayed in the field. If the value does not match the data value of an existing item,
* and the valueNotFoundText config option is defined, it will be displayed as the default field text.
* Otherwise the field will be blank (although the value will still be set).
* @param {String} value The value to match
*/
setValue : function(v){
var text = v;
if (v && this.mode == 'remote' && !this.store.isLoaded()) {
this.lastQuery = '';
this.store.load({
scope: this,
params: this.getParams(),
callback: function(){
this.setValue(v)
}
})
}
if(this.valueField){
var r = this.findRecord(this.valueField, v);
if(r){
text = r.data[this.displayField];
}else if(this.valueNotFoundText !== undefined){
text = this.valueNotFoundText;
}
}
this.lastSelectionText = text;
if(this.hiddenField){
this.hiddenField.value = v;
}
Ext.form.ComboBox.superclass.setValue.call(this, text);
this.value = v;
},
})

dan.plifeye
29 Aug 2008, 1:41 AM
Nice one for the override code devnull! Works well for me too! The ComboBox issue you mention has been annoying me since I started using ExtJS a few weeks ago.

devnull
29 Aug 2008, 8:01 AM
There are times that this will cause comboBoxes to make a load request when they really dont need to, but I havent pinned down any conditions to test for yet. Perhaps checking to see if 'displayField' and 'valueField' are equal...
In any case, it does not break functionality but rather just causes possible extra load requests leading to possible decreased performance in some cases. Even this should have limited impact, since the combos will still be rendered and available before these requests are made, with the combos then being updated as the load requests complete.

dearsina
29 Aug 2008, 5:07 PM
devnull, brilliant!

One comment, if you have two different combo fields using the same store, only one of them will be loaded. I solved that issue (or rather, circumvented it) by duplicating the store with a different store name.

devnull
2 Sep 2008, 8:11 AM
That would certainly require a bit more thought, since if I reuse a store it is always loaded during app init and then considered local. I am glad you were able to find a working solution though.

Anghram
19 Sep 2008, 12:46 AM
Sorry, but why is it only for remote combos? I got the same problem with local. I can't understand, why you are talking just about remote.

dearsina
26 Sep 2008, 7:04 AM
Anghram, I understand there is only this problem for remote, for local there is a solution. I'm not familiar with it myself, as I don't use local combos, but I think it should be floating around someone in the forums.

My suggestion? Switch to remote... :p

devnull
26 Sep 2008, 7:22 AM
This is to deal with the asynchronous nature of remote comboBoxes, local ones always have the data available and do not suffer from this problem. If you are having a problem with a local combo that you feel is similar, it would be best to ask about it in the help forum.

apemberton
29 Sep 2008, 3:46 PM
Devnull:

Very cool solution; I worked out something similar today.

ComboBox has an initValue function you can override, which only gets called when initally setting a value. This way, you don't have to add the isLoaded method, etc.


Ext.override(Ext.form.ComboBox,{
initValue : function(){
var initVal = null;
if(this.value !== undefined){
initVal = this.value;
}else if(this.el.dom.value.length > 0){
initVal = this.el.dom.value;
}
if(initVal != null){
if(this.mode == 'remote'){
var p = {};
p[this.valueField] = initVal;
this.store.load({
params: p
});
}
this.setValue(initVal);
}
}
});You could use this.queryParam in this line:
p[this.queryParam] = initVal; if you wanted to, but I'm actually sending the valueField parameter over to catch in my lookup service so that I can load the record directly by ID.

Enjoy,
Andy

devnull
30 Sep 2008, 9:06 AM
Good point, I will look into this. I really didnt like having to also modify Store, but felt the additional status indicator could be useful elsewhere anyway.

issameddine
21 Oct 2008, 12:32 AM
Hello,
I added this part of code, but it didn't work. perhaps it not in the good place. where i place it?

devnull
21 Oct 2008, 7:35 AM
Like any other override or extension, it should be loaded after the Ext includes, but before your own code.

HerrB
21 Oct 2008, 8:28 AM
And maybe a little fix, too.


Ext.override(Ext.form.ComboBox,{
initValue : function(){
var initVal = null;
if(this.value !== undefined){
initVal = this.value;
}else if(this.el.dom.value.length > 0){
initVal = this.el.dom.value;
}
if(initVal != null){
if(this.mode == 'remote'){
var p = {};
p[this.valueField] = initVal;
p['start'] = '0'; // Avoids NaN in initial paging
p['limit'] = this.pageSize; // Avoids NaN in initial paging

this.store.load({
scope: this,
params: p,
callback: function() {
this.setValue(initVal);
this.collapse(); // Just for IE6 (expands initial item)
}
});
} else {
this.setValue(initVal);
}
}
}
});

I'm a beginner - I'm sorry, if I have made it worse.

apemberton's solution is great, but may not work, as the load method is asynchronous and at the time setValue is executed, data doesn't have to be available. I have added setValue as callback for load (using the solution of devnull) and changed the now second this.setValue only to be executed, when in local mode.

A complete solution (which still may contain bugs I'm happy to fix) may look like this:

Ext.onReady(function(){

var ds = new Ext.data.JsonStore({
url: '/somepath/returningjson.asp',
method: 'POST',
root: 'codes',
totalProperty: 'totalCount',
id: 'post_id',
fields: [
{name: 'codeValue', mapping: 'cValue'},
{name: 'codeId', mapping: 'cId'},
{name: 'codeDescription', mapping: 'cDesc'}
]
});

// Custom rendering Template
var resultTpl = new Ext.XTemplate(
'<tpl for="."><div class="search-item">',
'<h3>{codeValue}</h3>',
'{codeDescription}<br />',
'</div></tpl>'
);

Ext.override(Ext.form.ComboBox, {
initValue : function() {
var initVal = null;

if (this.value !== undefined) {
initVal = this.value;
} else if(this.el.dom.value.length > 0) {
initVal = this.el.dom.value;
}

if (initVal != null) {
if (this.mode == 'remote') {
var p = {};
p[this.valueField] = initVal;
p['start'] = '0';
p['limit'] = this.pageSize;

this.store.load({
scope: this,
params: p,
callback: function() {
this.setValue(initVal);
this.collapse();
}
});
} else {
this.setValue(initVal);
}
}
}
});

var search = new Ext.form.ComboBox({
store: ds,
hiddenId: 'sCodeId',
hiddenName: 'sCodeId',
valueField: 'codeId',
value: '1234',
displayField: 'codeValue',
typeAhead: false,
loadingText: 'Searching...',
width: 400,
pageSize: 10,
hideTrigger: false,
tpl: resultTpl,
applyTo: 'search',
itemSelector: 'div.search-item',
});
});

For other beginners some more information:

If no other name is specified, ComboBox sends the search term as "query" in POST (maybe well-known, but I found it hard to get from the documentation)
apemberton's solution uses the value specified in "value" (here: 1234) as initial value
On initialization, the initial value is sent automatically to the server as "codeId" in POST (as "codeId" is the valueField in my example) - so you can decide in your "returningjson.asp" between a value in "query" (-> search a term) or "codeId" (-> return the exact record, specified by "value")


Thanks for the solutions, devnull and apemberton!

Regards,

HerrB

HerrB
22 Oct 2008, 8:12 AM
There is a little downfall: In IE6 the default value will be set, but the "found items" area will be shown immediately - with the correct content, but a page number of "NaN" (not a number).

I will search in the forum, but does someone have an idea to avoid this? Thx.

Regards,

HerrB

HerrB
22 Oct 2008, 8:19 AM
For NaN: You have to set start and limit on the initial query. I have updated the code above. Thx to the Forum.

That the results area is shown in IE6 remains. Any ideas?

Regards,

HerrB

HerrB
22 Oct 2008, 10:06 AM
Just force collapse in the callback setValue. Code updated.

Regards,
HerrB

azpoulton
24 Oct 2008, 1:38 PM
This fixed my remote combo box that was a converted select widget in an openACS ad_form when I was editing the data! thanks!



Ext.onReady(function(){

Ext.override(Ext.data.Store, {

// private

// Keeps track of the load status of the store. Set to true after a successful load event

loaded: false,

/**

* Returns true if the store has previously performed a successful load function.

* @return {Boolean} Whether the store is loaded.

*/

isLoaded: function(){

return this.loaded

},

// private

// Called as a callback by the Reader during a load operation.

loadRecords : function(o, options, success){

if(!o || success === false){

if(success !== false){

this.fireEvent("load", this, [], options);

}

if(options.callback){

options.callback.call(options.scope || this, [], options, false);

}

return;

}

var r = o.records, t = o.totalRecords || r.length;

if(!options || options.add !== true){

if(this.pruneModifiedRecords){

this.modified = [];

}

for(var i = 0, len = r.length; i < len; i++){

r[i].join(this);

}

if(this.snapshot){

this.data = this.snapshot;

delete this.snapshot;

}

this.data.clear();

this.data.addAll(r);

this.totalLength = t;

this.applySort();

this.fireEvent("datachanged", this);

}else{

this.totalLength = Math.max(t, this.data.length+r.length);

this.add(r);

}

this.loaded = true;

this.fireEvent("load", this, r, options);

if(options.callback){

options.callback.call(options.scope || this, r, options, true);

}

}

})



Ext.override(Ext.form.ComboBox,{

/**

* Sets the specified value into the field. If the value finds a match, the corresponding record text

* will be displayed in the field. If the value does not match the data value of an existing item,

* and the valueNotFoundText config option is defined, it will be displayed as the default field text.

* Otherwise the field will be blank (although the value will still be set).

* @param {String} value The value to match

*/

setValue : function(v){

var text = v;

if (v && this.mode == 'remote' && !this.store.isLoaded()) {

this.lastQuery = '';

this.store.load({

scope: this,

params: this.getParams(),

callback: function(){

this.setValue(v)

}

})

}

if(this.valueField){

var r = this.findRecord(this.valueField, v);

if(r){

text = r.data[this.displayField];

}else if(this.valueNotFoundText !== undefined){

text = this.valueNotFoundText;

}

}

this.lastSelectionText = text;

if(this.hiddenField){

this.hiddenField.value = v;

}

Ext.form.ComboBox.superclass.setValue.call(this, text);

this.value = v;

},

})



// Reference local blank image to prevent link back to extjs.com needed for images to work properly
Ext.BLANK_IMAGE_URL = '/resources/ext-2.2/resources/images/default/s.gif';
Ext.QuickTips.init();


// Ext.Msg.show({
// title:'Working',
// msg: 'Working',
// buttons:Ext.Msg.OK
// });


var unitstore = new Ext.data.JsonStore({
url:'database',
root:'results',
getTotalCount: 'total',
fields: ['unit_id','unit_name'],
baseParams:{task: "LIST"}

});

var converted = new Ext.form.ComboBox({
store: unitstore,
displayField: 'unit_name',
valueField: 'unit_id',
typeAhead: true,
triggerAction: 'all',
transform:'unit_id',
emptyText:'Select a unit...',
width:135,
forceSelection:true
});

converted.setValue('@current_unit_id@');


});

ojintoad
29 Dec 2008, 6:45 AM
Override code for initValue worked for me for a custom form, thanks for sharing.

jasonb885
15 Feb 2009, 5:07 PM
With devnull's solution, if the URL is not found, it will cause Ext too loop forever attempting to access the remote store.

Setting isLoaded anyway right before the first return in loadRecords reduces the load attempts to 2 from infinity.

Certainly, the best solution is to have the URL be available, but I did the client-side code first...

:-?

sergey.s
12 Mar 2009, 5:14 AM
yet another mixed solution

Ext.override(Ext.form.ComboBox, {
setValue: Ext.form.ComboBox.prototype.setValue.createSequence(function(v) {
var idx = this.store.find(this.valueField, v);

if (v && this.mode == 'remote' && idx == -1) {
var p = {};
p[this.valueField] = v;

this.store.load({
scope: this,
params: p,
callback: function() {
this.setValue(v);
this.collapse();
}
});
}
})
});

zilionis
5 Apr 2009, 7:30 AM
sergey.s:
Thanks, i liked your solution!