1. #1
    Sencha - Community Support Team Condor's Avatar
    Join Date
    Mar 2007
    Location
    The Netherlands
    Posts
    24,246
    Vote Rating
    82
    Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of

      0  

    Default 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
    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);
    	}
    });
    Example:

    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]
    	});
    });

  2. #2
    Ext User
    Join Date
    Mar 2008
    Posts
    14
    Vote Rating
    0
    kloffy is on a distinguished road

      0  

    Default


    Very nice solution. Thanks!

    I've taken the liberty to modify your solution slightly.

    PHP Code:
    Ext.namespace('Ext.ux.data');
    Ext.ux.data.CalcRecord = function(dataid) {
        
    Ext.ux.data.CalcRecord.superclass.constructor.call(thisdataid);
        
    this.updateCalculatedFields();
    }
    Ext.ux.data.CalcRecord.create = function(fieldscalculatedFields){
        var 
    Ext.extend(Ext.ux.data.CalcRecord, {});
        var 
    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 
    0len fields.lengthleni++){
            
    p.fields.add(new Ext.data.Field(fields[i]));
        }
        for(var 
    0len calculatedFields.lengthleni++){
            
    p.fields.add(new Ext.data.Field({namecalculatedFields[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.CalcRecordExt.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(namevalue) {
            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);
            }
        }
    }); 
    Example:

    PHP Code:
        function templateCalculate(template) {
            return function(
    record) {
                return 
    template.applyTemplate(record.data);
            };
        }

        var 
    record Ext.ux.data.CalcRecord.create([
            {
    name'first_name'mapping0},
            {
    name'last_name'mapping1},
            {
    name'age'mapping2}
        ], [
            {
    name'name'dependencies: ['first_name''last_name'], function: templateCalculate(new Ext.Template('{first_name}, <i>{last_name}</i>'))}
        ]); 

  3. #3
    Sencha - Ext JS Dev Team Animal's Avatar
    Join Date
    Mar 2007
    Location
    Notts/Redwood City
    Posts
    30,496
    Vote Rating
    44
    Animal has a spectacular aura about Animal has a spectacular aura about Animal has a spectacular aura about

      0  

    Default


    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];
                }
            }
        ]);

  4. #4
    Sencha - Community Support Team Condor's Avatar
    Join Date
    Mar 2007
    Location
    The Netherlands
    Posts
    24,246
    Vote Rating
    82
    Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of

      0  

    Default


    Quote Originally Posted by Animal View Post
    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:
    Nice addition, but that only helps when loading records. It won't help with records that are newly created, nor with changes made to the record.

    -- Moderator : This is incorrect. This does get executed when new records are created.

  5. #5
    Ext User
    Join Date
    Mar 2008
    Posts
    14
    Vote Rating
    0
    kloffy is on a distinguished road

      0  

    Default


    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(dataid) {
        
    Ext.ux.data.CalcRecord.superclass.constructor.call(thisdataid);
        
    this.updateCalculatedFields();
    }
    Ext.ux.data.CalcRecord.create = function(fieldscalculatedFields){
        var 
    Ext.extend(Ext.ux.data.CalcRecord, {});
        var 
    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 
    0len fields.lengthleni++){
            
    p.fields.add(new Ext.data.Field(fields[i]));
        }
        for(var 
    0len calculatedFields.lengthleni++){
            
    p.fields.add(new Ext.data.Field({namecalculatedFields[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.CalcRecordExt.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(namevalue) {
            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);
            }
        }
    }); 

  6. #6
    Sencha - Community Support Team Condor's Avatar
    Join Date
    Mar 2007
    Location
    The Netherlands
    Posts
    24,246
    Vote Rating
    82
    Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of Condor has much to be proud of

      0  

    Default


    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)

  7. #7
    Ext User
    Join Date
    Sep 2007
    Location
    Rothschild, WI
    Posts
    46
    Vote Rating
    0
    ziesemer is on a distinguished road

      0  

    Lightbulb


    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!

  8. #8
    Ext User
    Join Date
    Sep 2007
    Location
    Rothschild, WI
    Posts
    46
    Vote Rating
    0
    ziesemer is on a distinguished road

      0  

    Default


    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.
    Attached Files

  9. #9
    Sencha User Tasm's Avatar
    Join Date
    Nov 2007
    Location
    Kazakhstan, Almaty
    Posts
    10
    Vote Rating
    0
    Tasm is on a distinguished road

      0  

    Default


    Hi. Thanks for good extension.
    This is version with formula:

    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);
    	}
    });
    Usage example:
    Code:
    var MyRecord = Ext.ux.data.CalcRecord.create([
    	{name: 'value1', mapping: 0},
    	{name: 'value2', mapping: 1},
    	{name: 'sum', formula: '{value1} + {value2}', notDirty: true}
    ]);

  10. #10
    Sencha User
    Join Date
    Feb 2008
    Posts
    193
    Vote Rating
    -1
    hpet is an unknown quantity at this point

      0  

    Default


    Can I use this with JsonStore during fields declaration?