-
8 Mar 2008 2:26 AM #1Sencha - Community Support Team
- Join Date
- Mar 2007
- Location
- The Netherlands
- Posts
- 24,251
- Vote Rating
- 44
Calculated fields
Calculated fields
(works on all versions of Ext 1.x, 2,x and 3.x)
Until now I always used a renderer in a grid to display a value that was calculated from other values in the record. The problem with this solution is that the resulting column isn't sortable.
Since sorting is embedded in the store the only way to support sorting on a calculated field is to actually create the field and fill it with the calculated value.
To make this easier I created an extension to Record that allows for calculating fields.
CalcRecord.js
Example:Code:Ext.namespace('Ext.ux.data'); Ext.ux.data.CalcRecord = function(data, id) { Ext.ux.data.CalcRecord.superclass.constructor.call(this, data, id); this.calcFields(); } Ext.ux.data.CalcRecord.create = function(o){ var f = Ext.extend(Ext.ux.data.CalcRecord, {}); var p = f.prototype; p.fields = new Ext.util.MixedCollection(false, function(field){ return field.name; }); for(var i = 0, len = o.length; i < len; i++){ p.fields.add(new Ext.data.Field(o[i])); } f.getField = function(name){ return p.fields.get(name); }; return f; }; Ext.extend(Ext.ux.data.CalcRecord, Ext.data.Record, { set: function(name, value) { if(String(this.data[name]) == String(value)){ return; } this.dirty = true; if(!this.modified){ this.modified = {}; } if(typeof this.modified[name] == 'undefined'){ this.modified[name] = this.data[name]; } this.data[name] = value; this.calcFields(name); if(!this.editing && this.store){ this.store.afterEdit(this); } }, calcFields: function(name) { this.fields.each(function(field) { if ((field.name != name) && (typeof field.calc == 'function') && (!name || (!field.dependencies || field.dependencies.indexOf(name) != -1))) { var value = field.calc(this); if (!name || field.notDirty) { this.data[field.name] = value; } else { this.set(field.name, value); } } }, this); } });
Code:var MyRecord = Ext.ux.data.CalcRecord.create([ {name: 'value1', mapping: 0}, {name: 'value2', mapping: 1}, {name: 'sum', dependencies: ['value1', 'value2'], notDirty: true, calc: function(record) { return record.get('value1') + record.get('value2'); }} ]); var store = new Ext.data.Store({ reader: new Ext.data.ArrayReader({}, MyRecord), data: [[1, 2], [3, 4]] }); var grid = new Ext.grid.EditorGridPanel({ store: store, columns: [ {header: 'Value 1', dataIndex: 'value1', sortable: true, editor: new Ext.form.NumberField({allowBlank: false})}, {header: 'Value 2', dataIndex: 'value2', sortable: true, editor: new Ext.form.NumberField({allowBlank: false})}, {header: 'Sum', dataIndex: 'sum', sortable: true} ] }); Ext.onReady(function(){ new Ext.Viewport({ layout: 'fit', items: [grid] }); });
-
8 Mar 2008 3:45 AM #2
Very nice solution. Thanks!
I've taken the liberty to modify your solution slightly.
Example:PHP Code:Ext.namespace('Ext.ux.data');
Ext.ux.data.CalcRecord = function(data, id) {
Ext.ux.data.CalcRecord.superclass.constructor.call(this, data, id);
this.updateCalculatedFields();
}
Ext.ux.data.CalcRecord.create = function(fields, calculatedFields){
var f = Ext.extend(Ext.ux.data.CalcRecord, {});
var p = f.prototype;
p.fields = new Ext.util.MixedCollection(false, function(field){
return field.name;
});
p.calculatedFields = new Ext.util.MixedCollection(false, function(calculatedField){
return calculatedField.name;
});
for(var i = 0, len = fields.length; i < len; i++){
p.fields.add(new Ext.data.Field(fields[i]));
}
for(var i = 0, len = calculatedFields.length; i < len; i++){
p.fields.add(new Ext.data.Field({name: calculatedFields[i]['name']}));
p.calculatedFields.add(new Ext.data.Field(calculatedFields[i]));
}
f.getField = function(name){
return p.fields.get(name);
};
return f;
};
Ext.extend(Ext.ux.data.CalcRecord, Ext.data.Record, {
updateCalculatedFields: function(name){
var record = this;
this.calculatedFields.each(function(field){
if(!name || field['dependencies'].indexOf(name)!=-1){
record.data[field['name']] = field['function'](record);
}
});
},
set: function(name, value) {
if(String(this.data[name]) == String(value)){
return;
}
this.dirty = true;
if(!this.modified){
this.modified = {};
}
if(typeof this.modified[name] == 'undefined'){
this.modified[name] = this.data[name];
}
this.data[name] = value;
this.updateCalculatedFields(name);
if(!this.editing && this.store){
this.store.afterEdit(this);
}
}
});
PHP Code:function templateCalculate(template) {
return function(record) {
return template.applyTemplate(record.data);
};
}
var record = Ext.ux.data.CalcRecord.create([
{name: 'first_name', mapping: 0},
{name: 'last_name', mapping: 1},
{name: 'age', mapping: 2}
], [
{name: 'name', dependencies: ['first_name', 'last_name'], function: templateCalculate(new Ext.Template('{first_name}, <i>{last_name}</i>'))}
]);
-
11 Mar 2008 2:26 AM #3
In the next release, you should just be able to use a convert function which recieves the whole raw row object in the second parameter as well as the single mapped value:
Code:var record = Ext.data.Record.create([ {name: 'first_name', mapping: 0}, {name: 'last_name', mapping: 1}, {name: 'age', mapping: 2}, { name: 'name', mapping: 0, convert: function(val, data) { return val + " " + data[1]; } } ]);Search the forum: http://www.google.com/coop/cse?cx=01...%3Az7of1ufqccu
Read the docs too: http://extjs.com/deploy/dev/docs/
Scope: http://extjs.com/forum/showthread.ph...642#post257642
-
11 Mar 2008 2:29 AM #4
-
17 Mar 2008 8:02 AM #5
Here's a little update to my last version. Now, the calculated fields are flagged as modified whenever the underlying data changes.
PHP Code:Ext.namespace('Ext.ux.data');
Ext.ux.data.CalcRecord = function(data, id) {
Ext.ux.data.CalcRecord.superclass.constructor.call(this, data, id);
this.updateCalculatedFields();
}
Ext.ux.data.CalcRecord.create = function(fields, calculatedFields){
var f = Ext.extend(Ext.ux.data.CalcRecord, {});
var p = f.prototype;
p.fields = new Ext.util.MixedCollection(false, function(field){
return field.name;
});
p.calculatedFields = new Ext.util.MixedCollection(false, function(calculatedField){
return calculatedField.name;
});
for(var i = 0, len = fields.length; i < len; i++){
p.fields.add(new Ext.data.Field(fields[i]));
}
for(var i = 0, len = calculatedFields.length; i < len; i++){
p.fields.add(new Ext.data.Field({name: calculatedFields[i]['name']}));
p.calculatedFields.add(new Ext.data.Field(calculatedFields[i]));
}
f.getField = function(name){
return p.fields.get(name);
};
return f;
};
Ext.extend(Ext.ux.data.CalcRecord, Ext.data.Record, {
updateCalculatedFields: function(name){
var record = this;
this.calculatedFields.each(function(field){
if(!name || field['dependencies'].indexOf(name)!=-1){
var value = field['function'](record);
if(name)
{
if(String(record.data[field['name']]) == String(value)){
return;
}
if(!record.modified){
record.modified = {};
}
if(typeof record.modified[field['name']] == 'undefined'){
record.modified[field['name']] = record.data[field['name']];
}
}
record.data[field['name']] = value;
}
});
},
set: function(name, value) {
if(String(this.data[name]) == String(value)){
return;
}
this.dirty = true;
if(!this.modified){
this.modified = {};
}
if(typeof this.modified[name] == 'undefined'){
this.modified[name] = this.data[name];
}
this.data[name] = value;
this.updateCalculatedFields(name);
if(!this.editing && this.store){
this.store.afterEdit(this);
}
}
});
-
18 Mar 2008 12:51 AM #6Sencha - Community Support Team
- Join Date
- Mar 2007
- Location
- The Netherlands
- Posts
- 24,251
- Vote Rating
- 44
I've updated my original version with some of your ideas.
The field config can now contain:
calc: a function that returns the calculated value (parameter: the current record)
dependencies: an array of fieldnames used in the calculation (calculated on every change if not specified)
notDirty: field will not become dirty if the calculated value changes (default false)
-
10 Apr 2008 1:25 PM #7
This looks like some great progress!
I was looking for something very much like this for use with Ext.form.ComboBox. (See also http://extjs.com/forum/showthread.php?t=26706.)
Unlike some of the workarounds that can be put in place for ComboBox, any I've seen so far have severe limitations. For example, the template ("tpl") property is a great start, but it only affects the drop-down values. Sure, additional listeners can be added to update the currently displayed text, but then options such as the look ahead filter, etc., still don't take the "calculated field" into effect. I definitely think implementing calculated fields at the store level properly accounts for all this and a number of other issues.
However, especially if this is something that is being looked at for inclusion into a future release of Ext, could an option be used to override get(...) instead of set(...)? Maybe both approaches have a place, but I would definitely prefer the get(...) approach, as it offers significant memory savings. With the set(...) approach, every additional field results in an additional object created for every row cached in the store, and that adds up pretty quickly, especially considering that the additional data is probably completely redundant, i.e. calculated from other fields.
I understand that sorting is a concern. However, if it's embedded in the store, and doesn't currently work with the get(...) override approach, couldn't the store be modified/fixed to be sure that it calls get(...)? (I haven't looked to see what it is currently doing.) Especially if this is being considered for inclusion into Ext, the Store could do a check to see if the field is calculated or not, and then adjust for it as/if needed.
Thanks for your consideration!
-
10 Apr 2008 2:44 PM #8
Before anyone rightfully destroys my last idea...
After I almost completed my own proposed version of a get(...) overridden record, I found that at least with the current API, it's unfortunately not quite feasible.
The "data" object hash on Record is publicly accessible - and often used directly, it seems. This means that there is no guarantee that an overridden get(...) method will ever be called.
It does seem, though, that this also affects Condor's originally proposed version. If I can access the "data " object hash directly to get a value and bypass get(...), what's to stop anyone from setting a value directly in the same way? (set(...) will never be called.)
Maybe the "data" object hash needs to be deprecated in favor of the get(...) and set(...) methods to allow for these type of enhancements?
If we can't get something like this working, could something be considered to better handle the situation with utilizing a "calculated field" on a ComboBox? Even if the 4 current references to the "data" object were to be replaced with the getters/setters, then it would at least allow someone to utilize extended Record types...
I'm attaching what I thought was going to work in case it can benefit anyone else.
-
22 Apr 2008 2:58 AM #9
Hi. Thanks for good extension.
This is version with formula:
Usage example:Code:Ext.namespace('Ext.ux.data'); Ext.ux.data.CalcRecord = function(data, id){ Ext.ux.data.CalcRecord.superclass.constructor.call(this, data, id); this.calcFields(); } Ext.ux.data.CalcRecord.create = function(o){ var f = Ext.extend(Ext.ux.data.CalcRecord, {}); var p = f.prototype; p.fields = new Ext.util.MixedCollection(false, function(field){ return field.name; }); for (var i = 0, len = o.length; i < len; i++) { if (o[i].formula) { var re = /(\{([^{]*)\})/gi var dep = []; o[i].calcTpl = new Ext.XTemplate("{[" + o[i].formula.replace(re, function(s1, s2, s3) { if (dep.indexOf(s3) == -1) dep.push(s3); return "values." + s3; }) + "]}"); o[i].dependencies = dep; o[i].calc = function(record) { return this.calcTpl.apply(record.data); }; } p.fields.add(new Ext.data.Field(o[i])); } f.getField = function(name){ return p.fields.get(name); }; return f; }; Ext.extend(Ext.ux.data.CalcRecord, Ext.data.Record, { set: function(name, value){ if (String(this.data[name]) == String(value)) { return; } this.dirty = true; if (!this.modified) { this.modified = {}; } if (typeof this.modified[name] == 'undefined') { this.modified[name] = this.data[name]; } this.data[name] = value; this.calcFields(name); if (!this.editing && this.store) { this.store.afterEdit(this); } }, calcFields: function(name){ this.fields.each(function(field){ if ((field.name != name) && (typeof field.calc == 'function') && (!name || (!field.dependencies || field.dependencies.indexOf(name) != -1))) { var value = field.calc(this); if (!name || field.notDirty) { this.data[field.name] = value; } else { this.set(field.name, value); } } }, this); } });
Code:var MyRecord = Ext.ux.data.CalcRecord.create([ {name: 'value1', mapping: 0}, {name: 'value2', mapping: 1}, {name: 'sum', formula: '{value1} + {value2}', notDirty: true} ]);
-
4 Sep 2008 4:10 AM #10
Can I use this with JsonStore during fields declaration?


Reply With Quote


