PDA

View Full Version : Calculated fields



Condor
8 Mar 2008, 2:26 AM
(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

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:


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

kloffy
8 Mar 2008, 3:45 AM
Very nice solution. Thanks!

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


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



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

Animal
11 Mar 2008, 2:26 AM
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:



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

Condor
11 Mar 2008, 2:29 AM
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.

kloffy
17 Mar 2008, 8:02 AM
Here's a little update to my last version. Now, the calculated fields are flagged as modified whenever the underlying data changes.


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

Condor
18 Mar 2008, 12:51 AM
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)

ziesemer
10 Apr 2008, 1:25 PM
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!

ziesemer
10 Apr 2008, 2:44 PM
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.

Tasm
22 Apr 2008, 2:58 AM
Hi. Thanks for good extension.
This is version with formula:



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:


var MyRecord = Ext.ux.data.CalcRecord.create([
{name: 'value1', mapping: 0},
{name: 'value2', mapping: 1},
{name: 'sum', formula: '{value1} + {value2}', notDirty: true}
]);

hpet
4 Sep 2008, 4:10 AM
Can I use this with JsonStore during fields declaration?

Condor
4 Sep 2008, 8:20 AM
Yes, the JsonStore fields config option can contain a Ext.ux.data.CalcRecord instead of an array.

wwwtd
12 Oct 2008, 6:48 PM
how to use Ext.ux.data.CalcRecord and this issue to get column and row 's totle value?

I did not get all totle values that I want .someone can help me and give me an example?
[CODE]<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Ext.grid.GridSummary Plugin Example</title>
<link rel="stylesheet" href="../js/ext/resources/css/ext-all.css"/>
<link rel="stylesheet" href="../css/Ext.ux.grid.GridSummary.css"/>
<script src="../js/ext/adapter/ext/ext-base.js"></script>
<script src="../js/ext/ext-all.js"></script>
<script src='../js/Ext.ux/Ext.ux.grid.GridSummary.js'></script><!-- Ext.ux.grid.GridSummary plugin -->
<script src='../js/Ext.ux/Ext.ux.grid.CalcRecord.js'></script>
<script>
Ext.util.Format.usMoney = function(v) { // override Ext.util.usMoney
v = Ext.num(v, 0); // ensure v is a valid numeric value, otherwise use 0 as a base (fixes $NaN.00 appearing in summaryRow when no records exist)
v = (Math.round((v - 0) * 100)) / 100;
v = (v == Math.floor(v)) ? v + ".00" : ((v * 10 == Math.floor(v * 10)) ? v + "0" : v);
v = String(v);
var ps = v.split('.');
var whole = ps[0];
var sub = ps[1] ? '.'+ ps[1] : '.00';
var r = /(\d+)(\d{3})/;
while (r.test(whole)) {
whole = whole.replace(r, '$1' + ',' + '$2');
}
v = whole + sub;
if (v.charAt(0) == '-') {
return '-$' + v.substr(1);
}
return "$" + v;
}
Ext.onReady(function() {
// custom renderer example
function change(val) {
if (val > 0) {
return '<span style="color:green;">' + val + '</span>';
} else if (val < 0) {
return '<span style="color:red;">' + val + '</span>';
}
return val;
}
// custom renderer example
function pctChange(val) {
if (val > 0) {
return '<span style="color:green;">' + val + '</span>';
} else if(val < 0) {
return '<span style="color:red;">' + val + '</span>';
}
return val;
}

// custom summary renderer example
function totalCompanies(v, params, data) {
return v? ((v === 0 || v > 1) ? '(' + v +' Companies)' : '(1 Company)') : '';
}

// custom summary renderer example
function averageChange(v, params, data) {
return v? ('Average: ' + v) : '';
}
var fields="{name: 'price', type: 'float'},{name: 'a001', type: 'float'},"
+"{name: 'pctChange', type: 'float'},"
+"{name:'all',type:'float',formula: '{price}+{a001}+{pctChange}', notDirty: true}";
var myfields=eval('([' + fields + '])');


var myrecord=Ext.ux.data.CalcRecord.create(myfields);
// create the data store
/* var store = new Ext.data.SimpleStore({
fields: [
{name: 'company'},
{name: 'price', type: 'float'},
{name: 'change', type: 'float'},
{name: 'pctChange', type: 'float'},
{name: 'lastChange', type: 'date', dateFormat: 'n/j h:ia'}
]
});*/
// store.loadData(myData);
var paystore = new Ext.data.Store({
proxy: new Ext.data.HttpProxy({url:"test.jsp"}),
reader:new Ext.data.JsonReader({totalProperty:"results",root: "rows"},myrecord)
});

var summary = new Ext.ux.grid.GridSummary();

// create the Grid
var grid = new Ext.grid.EditorGridPanel({
renderTo: 'grid-example',
store: paystore,
columns: [
new Ext.grid.RowNumberer(),
{header: "Price", width: 75, sortable: true, renderer: 'usMoney', dataIndex: 'price', summaryType: 'sum', editor: new Ext.form.NumberField({allowDecimals: true})},
{header: "ces", width: 100, sortable: true, renderer: change, dataIndex: 'a001', summaryType: 'sum', summaryRenderer: averageChange},
{header: "pctChange", width: 75, sortable: true, renderer: pctChange, dataIndex: 'pctChange'},
{header: "tot", width: 300, sortable: true,dataIndex: 'all',summaryType: 'sum',renderer:Ext.util.Format.usMoney}
],
plugins: [summary], // have the EditorGridPanel use the GridSummary plugin
bbar: new Ext.PagingToolbar({
pageSize: 20,
store: paystore,
displayInfo: true,
displayMsg: '

NightAvatar
14 Jan 2009, 3:04 AM
Condor,

This is an excellent extension, and very useful!

One problem I am having is that when the default value of the combos text field is set to a value from the array, the combobox changes the value to the full value of the combined arrays.

Example:

My array for the combobox looks like this:


// set accounts for dropdown combobox
var toAccounts = [ // first value is account number, second is owner name
['6001 00 00028','Some crazy name'],['6001 00 00036','Endret navn lagres'],['6001 00 00109','A\u00E6\u00F8\u00E5 Endret navn'],['6001 00 00125','Noe ulovelig'],['6001 00 00176','gsgsgs test'],['6001 00 00184','Freedom ssss'],['6001 00 00192','Name and numbers'],['6001 00 00206','could we have some?'],['6001 00 00214',"Don't think this well work"],['6001 00 00222','Name6 wuethb'],['6001 00 00230','Pathetic bird'],['6001 00 00249','Personal trainer'],['6001 00 00257','Bedrift of columbia'],['6001 00 00265','Bosnia Builders'],['6001 00 00273','eColissionaire'],['6001 00 00281','Name of fortress'],['6001 00 00303','canopy crater'],['6001 00 00494','Steamboat Willie'],['6001 02 41033','Bedrift test'],['6001 03 00048','Ny konto lagres'],['6001 04 37116','Ola Nordman'],['6001 06 89557','Bedrift kyrre Hansen'],['6003 08 49496','Richard Carlton Harris II'],['6003 12 34568','Bedrift brrrrrr'],['6003 23 45673','Bedrift 5 test test'],['6003 34 56789','Bedrift 5 test igjen'],['6546 08 01006','Bedrift Test'],['7001 12 34567','Ann-Kristin Br\u00F8endret AB'],['7855 05 03017','Dette er en skattekonto med ve'],['8200 97 51307','Bedrift kid required'],['9760 05 16993','Aftenposten-2']
]When the user selects from the combobox, the account number (first value in the array) is supposed to show in the combos field, and the owner name (second value in array) is supposed to be moved to another field.

I got all that working correctly but when (in the html) my combobox field has a default value of an account number, the browser shows it as a value of the account number AND name (combining both arrays)

Example:

<input type="text" id="combobox" value="6003 08 49496" />shows in the browser as though it has value="6003 08 49496 Richard Carlton Harris II" . That is, it combines both arrays and shows the value as it will appear in combobox list.

How can I prevent the combobox from overwriting my default value?

I still want the list to show both, but not the input field itself.

The code I use for the combobox looks like this:


Ext.onReady(function(){

var fromAccountRecord = Ext.ux.data.CalcRecord.create([
{name: 'acct', mapping: 0},
{name: 'name', mapping: 1},
{name: 'display', dependencies: ['acct', 'name'], notDirty: true, calc: function(record) {
return record.get('acct') + ' ' + record.get('name');
}}
]);

// using CalcRecord.js
var toAccountRecord = Ext.ux.data.CalcRecord.create([
{name: 'recAcct', mapping: 0},
{name: 'recName', mapping: 1},
{name: 'display', dependencies: ['recAcct', 'recName'], notDirty: true, calc: function(record) {
return record.get('recAcct') + ' ' + record.get('recName');
}}
]);

// fromAcct
var comboFraKonto = new Ext.ux.form.ComboBoxMatch({
store: new Ext.data.Store({
reader: new Ext.data.ArrayReader({}, fromAccountRecord),
data: debitAccounts
}),
displayField: 'display',
valueField: 'acct',
mode: 'local',
triggerAction: 'all',
applyTo: 'fraKonto',
typeAhead: true,
autoSelect: true,
forceSelection: true,
anyMatch: true,
queryIgnoreCase: true,
matchWordStart: true,
width:220,
maxHeight:200,
// specific regexp to ignore space
createValueMatcher: function(value) {
value = String(value).replace(/\s*/g, '');
if(Ext.isEmpty(value, false)){
return new RegExp('^');
}
value = Ext.escapeRe(value.split('').join('\\s*')).replace(/\\\\s\\\*/g, '\\s*');
return new RegExp('\\b(' + value + ')', 'i');
}
});

// toAccount
var comboToAccount = new Ext.ux.form.ComboBoxMatch({
store: new Ext.data.Store({
reader: new Ext.data.ArrayReader({}, toAccountRecord),
data: creditAccounts
}),
displayField: 'display',
valueField: 'recAcct',
mode: 'local',
triggerAction: 'all',
applyTo: 'toAcct',
forceSelection: false,
anyMatch: true,
queryIgnoreCase: true,
matchWordStart: true,
width:122,
listWidth:360,
maxHeight:200,
maxLength: 13,
// specific regexp to ignore space
createValueMatcher: function(value) {
value = String(value).replace(/\s*/g, '');
if(Ext.isEmpty(value, false)){
return new RegExp('^');
}
value = Ext.escapeRe(value.split('').join('\\s*')).replace(/\\\\s\\\*/g, '\\s*');
return new RegExp('\\b(' + value + ')', 'i');
},
listeners: {
scope: this,
select: function(c,r) {
var el = Ext.getDom('creditorName'), en = Ext.getDom('toAcct');
en.value = r.get('recAcct');
el.value = r.get('recName');
el.focus.defer(10, el);
}
}
});
});

Condor
14 Jan 2009, 5:10 AM
Again, something that has nothing to do with CalcRecord...

You want to display both the account number and name in the dropdown and you also want to search in these fields, but you only want to show the account number in the field?

You would have to change ComboBoxMatch to do this. It currently assumes that you want to see the displayField in the dropdown and search in the display field. But you want the display field to be the account number and display/search the combined field.

Example:

Ext.ns('Ext.ux.form');
Ext.ux.form.ComboBoxMatch = Ext.extend(Ext.form.ComboBox, {
anyMatch: true,
caseSensitive: false,
createValueMatcher: function(value) {
if(Ext.isEmpty(value, false)){
return new RegExp('^');
}
value = Ext.escapeRe(String(value));
return new RegExp((this.anyMatch === true ? '' : '^') + '(' + value + ')', this.caseSensitive ? '' : 'i');
},
prepareData : function(data) {
var result = Ext.apply({}, data);
result[this.searchField || this.displayField] = data[this.searchField || this.displayField].replace(this.createValueMatcher(this.getRawValue()), function(a, b){
if (typeof b != 'string') {
return '';
}
return '<span class="mark-combo-match">' + b + '</span>'
});
return result;
},
initList : function(){
Ext.ux.form.ComboBoxMatch.superclass.initList.apply(this, arguments);
this.view.prepareData = this.prepareData.createDelegate(this);
},
doQuery : function(q, forceAll){
if(q === undefined || q === null){
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.searchField || this.displayField, this.createValueMatcher(q));
}
this.onLoad();
}else{
this.store.baseParams[this.queryParam] = q;
this.store.load({
params: this.getParams(q)
});
this.expand();
}
}else{
this.selectedIndex = -1;
this.onLoad();
}
}
}
});
(set dipslayField:'acct' and searchField:'display')

NightAvatar
14 Jan 2009, 7:08 AM
Thanks for the help.

When I do as you suggest, the combo list only shows the account number.

In addition to your code above, I have this for my combos:


var comboFraKonto = new Ext.ux.form.ComboBoxMatch({
store: new Ext.data.Store({
reader: new Ext.data.ArrayReader({}, fromAccountRecord),
data: debitAccounts
}),
searchField: 'display',
displayField: 'acct',
valueField: 'acct',
mode: 'local',
triggerAction: 'all',
applyTo: 'fraKonto',
typeAhead: true,
autoSelect: true,
forceSelection: true,
anyMatch: true,
queryIgnoreCase: true,
matchWordStart: true,
width:220,
maxHeight:200,
// specific regexp to ignore space
createValueMatcher: function(value) {
value = String(value).replace(/\s*/g, '');
if(Ext.isEmpty(value, false)){
return new RegExp('^');
}
value = Ext.escapeRe(value.split('').join('\\s*')).replace(/\\\\s\\\*/g, '\\s*');
return new RegExp('\\b(' + value + ')', 'i');
}
});
If I do it with displayField: 'display' the list shows correctly but then the value in the field is also both the account and name (only after the page renders).

Am I supposed to define a new template?

Condor
14 Jan 2009, 8:05 AM
Yes, I forgot to mention that, you need a tpl that contains {display} instead of the displayField.

NightAvatar
15 Jan 2009, 12:21 AM
Now it strips all spaces from the display, and submits that as the valueField.

I know I must be doing something wrong, so I really appreciate you helping me work it out. Here is the code (same as above except changes in red):


Ext.onReady(function(){

var fromAccountRecord = Ext.ux.data.CalcRecord.create([
{name: 'acct', mapping: 0},
{name: 'name', mapping: 1},
{name: 'display', dependencies: ['acct', 'name'], notDirty: true, calc: function(record) {
return record.get('acct') + ' ' + record.get('name');
}}
]);

// using CalcRecord.js
var toAccountRecord = Ext.ux.data.CalcRecord.create([
{name: 'recAcct', mapping: 0},
{name: 'recName', mapping: 1},
{name: 'display', dependencies: ['recAcct', 'recName'], notDirty: true, calc: function(record) {
return record.get('recAcct') + ' ' + record.get('recName');
}}
]);

// fromAcct
var comboFraKonto = new Ext.ux.form.ComboBoxMatch({
store: new Ext.data.Store({
reader: new Ext.data.ArrayReader({}, fromAccountRecord),
data: debitAccounts
}),
searchField: 'display',
displayField: 'acct',
valueField: 'acct',
mode: 'local',
triggerAction: 'all',
applyTo: 'fraKonto',
typeAhead: true,
autoSelect: true,
forceSelection: true,
anyMatch: true,
queryIgnoreCase: true,
matchWordStart: true,
width:220,
maxHeight:200,
// specific regexp to ignore space
createValueMatcher: function(value) {
value = String(value).replace(/\s*/g, '');
if(Ext.isEmpty(value, false)){
return new RegExp('^');
}
value = Ext.escapeRe(value.split('').join('\\s*')).replace(/\\\\s\\\*/g, '\\s*');
return new RegExp('\\b(' + value + ')', 'i');
}
});

// toAccount
var comboToAccount = new Ext.ux.form.ComboBoxMatch({
store: new Ext.data.Store({
reader: new Ext.data.ArrayReader({}, toAccountRecord),
data: creditAccounts
}),
searchField: 'display',
displayField: 'recAcct',
valueField: 'recAcct',
mode: 'local',
triggerAction: 'all',
applyTo: 'toAcct',
forceSelection: false,
anyMatch: true,
queryIgnoreCase: true,
matchWordStart: true,
width:122,
listWidth:360,
maxHeight:200,
maxLength: 13,
// specific regexp to ignore space
createValueMatcher: function(value) {
value = String(value).replace(/\s*/g, '');
if(Ext.isEmpty(value, false)){
return new RegExp('^');
}
value = Ext.escapeRe(value.split('').join('\\s*')).replace(/\\\\s\\\*/g, '\\s*');
return new RegExp('\\b(' + value + ')', 'i');
},
listeners: {
scope: this,
select: function(c,r) {
var el = Ext.getDom('creditorName'), en = Ext.getDom('toAcct');
en.value = r.get('recAcct');
el.value = r.get('recName');
el.focus.defer(10, el);
}
}
})
});

And the template etc in the custom combo code:


// Fixes percentage floating container bug
Ext.override(Ext.form.ComboBox, {
initList : function(){
if(!this.list){
var cls = 'x-combo-list';
this.list = new Ext.Layer({
shadow: this.shadow, cls: [cls, this.listClass].join(' '), constrain:false
});
var lw = this.listWidth || Math.max(this.wrap.getWidth(), this.minListWidth);
this.list.setSize(lw, 0);
this.list.swallowEvent('mousewheel');
this.assetHeight = 0;
if(this.title){
this.header = this.list.createChild({cls:cls+'-hd', html: this.title});
this.assetHeight += this.header.getHeight();
}
this.innerList = this.list.createChild({cls:cls+'-inner'});
this.innerList.on('mouseover', this.onViewOver, this);
this.innerList.on('mousemove', this.onViewMove, this);
this.innerList.setWidth(lw - this.list.getFrameWidth('lr'));
if(this.pageSize){
this.footer = this.list.createChild({cls:cls+'-ft'});
this.pageTb = new Ext.PagingToolbar({
store:this.store,
pageSize: this.pageSize,
renderTo:this.footer
});
this.assetHeight += this.footer.getHeight();
}
if(!this.tpl){
this.tpl = '<tpl for="."><div class="'+cls+'-item">{display}</div></tpl>';
}
this.view = new Ext.DataView({
applyTo: this.innerList,
tpl: this.tpl,
singleSelect: true,
selectedClass: this.selectedClass,
itemSelector: this.itemSelector || '.' + cls + '-item'
});
this.view.on('click', this.onViewClick, this);
this.bindStore(this.store, true);
if(this.resizable){
this.resizer = new Ext.Resizable(this.list, {
pinned:true, handles:'se'
});
this.resizer.on('resize', function(r, w, h){
this.maxHeight = h-this.handleHeight-this.list.getFrameWidth('tb')-this.assetHeight;
this.listWidth = w;
this.innerList.setWidth(w - this.list.getFrameWidth('lr'));
this.restrictHeight();
}, this);
this[this.pageSize?'footer':'innerList'].setStyle('margin-bottom', this.handleHeight+'px');
}
}
}
});


// The custom combobox
Ext.ns('Ext.ux.form');
Ext.ux.form.ComboBoxMatch = Ext.extend(Ext.form.ComboBox, {
anyMatch: true,
caseSensitive: false,
createValueMatcher: function(value) {
if(Ext.isEmpty(value, false)){
return new RegExp('^');
}
value = Ext.escapeRe(String(value));
return new RegExp((this.anyMatch === true ? '' : '^') + '(' + value + ')', this.caseSensitive ? '' : 'i');
},
prepareData : function(data) {
var result = Ext.apply({}, data);
result[this.searchField || this.displayField] = data[this.searchField || this.displayField].replace(this.createValueMatcher(this.getRawValue()), function(a, b){
if (typeof b != 'string') {
return '';
}
return '<span class="mark-combo-match">' + b + '</span>'
});
return result;
},
initList : function(){
Ext.ux.form.ComboBoxMatch.superclass.initList.apply(this, arguments);
this.view.prepareData = this.prepareData.createDelegate(this);
},
doQuery : function(q, forceAll){
if(q === undefined || q === null){
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.searchField || this.displayField, this.createValueMatcher(q));
}
this.onLoad();
}else{
this.store.baseParams[this.queryParam] = q;
this.store.load({
params: this.getParams(q)
});
this.expand();
}
}else{
this.selectedIndex = -1;
this.onLoad();
}
}
}
,initEvents : function(){
Ext.form.ComboBox.superclass.initEvents.call(this);
this.keyNav = new Ext.KeyNav(this.el, {
"up" : function(e){
this.inKeyMode = true;
if(this.selectedIndex > 0){
this.selectPrev();
}else{
this.collapse();
}
},
"down" : function(e){
if(!this.isExpanded()){
this.onTriggerClick();
this.selectNext();
}else{
this.inKeyMode = true;
this.selectNext();
}
},
"enter" : function(e){
this.onViewClick();
this.delayedCheck = true;
this.unsetDelayCheck.defer(10, this);
},
"right" : function(e) {
if (isLast()) this.onViewClick(false);
return true;
},
"esc" : function(e){
this.collapse();
},
"tab" : function(e){
this.onViewClick(false);
return true;
},
scope : this,
doRelay : function(foo, bar, hname){
if(hname == 'down' || this.scope.isExpanded()){
return Ext.KeyNav.prototype.doRelay.apply(this, arguments);
}
return true;
},
forceKeyDown : true
});
this.queryDelay = Math.max(this.queryDelay || 10,
this.mode == 'local' ? 10 : 250);
this.dqTask = new Ext.util.DelayedTask(this.initQuery, this);
if(this.typeAhead){
this.taTask = new Ext.util.DelayedTask(this.onTypeAhead, this);
}
if((this.editable !== false) && !this.enableKeyEvents) {
this.el.on("keyup", this.onKeyUp, this);
}
if(this.forceSelection){
this.on('blur', this.doForce, this);
}
},
onKeyUp : function(e){
if(this.editable !== false && !e.isSpecialKey()){
this.lastKey = e.getKey();
this.dqTask.delay(this.queryDelay);
}
Ext.form.ComboBox.superclass.onKeyUp.call(this, e);
},
onLoad : function(){
if(!this.hasFocus){
return;
}
if(this.store.getCount() > 0){
this.expand();
this.restrictHeight();
if(this.lastQuery == this.allQuery){
if(this.editable){
this.el.dom.select();
}
if(this.forceSelection && !this.selectByValue(this.value, true)){
this.select(0, true);
}
}else{
if(this.forceSelection){
this.selectNext();
}
if(this.typeAhead && this.lastKey != Ext.EventObject.BACKSPACE && this.lastKey != Ext.EventObject.DELETE){
this.taTask.delay(this.typeAheadDelay);
}
}
}else{
this.onEmptyResults();
}
},
onViewClick : function(doFocus){
var index = this.view.getSelectedIndexes()[0];
var r = this.store.getAt(index);
if(r){
this.onSelect(r, index);
} else {
this.collapse();
}
if(doFocus !== false){
this.el.focus();
}
}
});Yes, that's a lot of code and most of it is irrelevant to this problem, but I wanted you to see everything. ;-)

Thanks again for helping me figure this out! :)

Condor
15 Jan 2009, 1:03 AM
1. You don't need to specify a valueField if it is the same as the displayField.
2. Why are you overriding ComboBox.initList? Instead use:

var comboFraKonto = new Ext.ux.form.ComboBoxMatch({
tpl: '<tpl for="."><div class="x-combo-list-item">{display}</div></tpl>',
..
});

NightAvatar
15 Jan 2009, 1:19 AM
Why are you overriding ComboBox.initList?
Because of a floating display error that I reported and you found a fix for (http://extjs.com/forum/showthread.php?p=253492#post253492). That is the only reason.


var comboFraKonto = new Ext.ux.form.ComboBoxMatch({
tpl: '<tpl for="."><div class="x-combo-list-item">{display}</div></tpl>',
..
});Where do I place that code?

I removed valueField and it works perfect now, thanks!! :)

Condor
15 Jan 2009, 3:39 AM
If would leave the tpl default in initList as it was:

this.tpl = '<tpl for="."><div class="'+cls+'-item">{' + this.displayField + '}</div></tpl>';

and specify your own tpl in the combobox config:

var comboFraKonto = new Ext.ux.form.ComboBoxMatch({
tpl: '<tpl for="."><div class="x-combo-list-item">{display}</div></tpl>',
..
});

mjlecomte
6 Feb 2009, 7:35 AM
If you already know, you might consider updating the title of this thread if it works with other versions than 2.0.2. 2.2.1? 3.0a? If so, maybe remove the version from the title and just show the versions within the first post? Just a suggestion.

mjlecomte
13 Feb 2009, 5:07 PM
Do you have any thoughts on implementing a calculated record?

You title the thread Calculated fields. But the class is called Calculated record.

In your original example you have:


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


So really you are calculating per field. So I'm not trying to beat you up with semantics of the name. But what I'm after is actually to do a calculation on the record.

To keep the use case similar to what you have, let's say there is another field that would be twice the value of adding the first two fields. My point here is that the fourth field should not have to repeat the calculation. If you edit one of the first two fields one function would be called to calculate the record. From there other fields in that record would be updated at that time.

mjlecomte
13 Feb 2009, 8:47 PM
So here's my crack at what I described above. Something I noticed while developing this is that I think the original code for calcFields has a recursive call for set() if notDirty is true for the field. This seems like a waste.

So what I did was to just update the value and then make the field dirty. I don't know that calling set() is needed, or even appropriate on these calculated fields because it would be updating the store to be modified and calling afterEdit on the store (again)....and repeating the calc yet again! (I think this causes exponential loop based on number of fields)

CalcRecord.js


Ext.namespace('Ext.ux.data');

Ext.ux.data.CalcRecord = function(data, id){
Ext.ux.data.CalcRecord.superclass.constructor.call(this, data, id);
console.info('about to calc (inside constructor)');
this.calc();
}

/**
* Generate a constructor for a specific Record layout.
*/
Ext.ux.data.CalcRecord.create = function(o, calc){
var cfg = Ext.apply({}, calc, {
calcMode: 'fields'
});
var f = Ext.extend(Ext.ux.data.CalcRecord, cfg);
// var f = Ext.extend(Ext.ux.data.CalcRecord, calc || {});

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.calc(name);
if (!this.editing && this.store) {
this.store.afterEdit(this);
}
},
calc: function(name){
if (this.calcMode == 'fields') {
this.calcFields(name);
} else {
this.calcRecord(name);
}
},
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) {
// do not show calculated field as dirty:
this.data[field.name] = value;
}
else {
this.set(field.name, value);
}
}
},
this
);
},
calcRecord: function(){
// get the results
var data = this.calcFn(this.data);

// update the record
if (this.trackDirty) {
for(var name in data){
this.data[name] = data[name];
if (this.trackDirty.indexOf(name) !== -1) {
// show calculated field as dirty:
if (!this.modified) {
this.modified = {};
}
if (typeof this.modified[name] == 'undefined') {
this.modified[name] = this.data[name];
}
}
}
} else {
Ext.apply(this.data, data);
}
}
});



<html>
<head>
<title>Calculated Records</title>
<link rel="stylesheet" type="text/css" href="../../resources/css/ext-all.css" />
<script type="text/javascript" src="../../adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="../../ext-all-debug.js"></script>
<script type="text/javascript" src="CalcRecord.js"></script>

<script type="text/javascript">
Ext.onReady(function(){
// custom function for calculations
var myCalc= function (data) {
var result = {};
result.sum = data.w + data.h;
result.avg = result.sum/2;
return result;
};
var MyRecord = Ext.ux.data.CalcRecord.create([
{name: 'w', mapping: 0},
{name: 'h', mapping: 1},
{
name: 'sum'
/*
,
dependencies: ['w', 'h'],
notDirty: true,
calc: function(record) {
return record.get('w') + record.get('h');
}
*/
},
{
name: 'avg'
/*
,
dependencies: ['w', 'h'],
notDirty: true,
calc: function(record) {
return record.get('sum')/2;
}
*/
}
],
// optional config for record calculations
{
calcFn: myCalc, // required, custom function for calculations
calcMode: 'record', // optional, default = 'fields'
trackDirty: ['sum'] // array of fields to show dirty (optional, default = false)
}
);
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: 'Width', dataIndex: 'w', sortable: true, editor: new Ext.form.NumberField({allowBlank: false})},
{header: 'Height', dataIndex: 'h', sortable: true, editor: new Ext.form.NumberField({allowBlank: false})},
{header: 'Sum', dataIndex: 'sum', sortable: true},
{header: 'Avg', dataIndex: 'avg', sortable: true}
]
});
new Ext.Viewport({
layout: 'fit',
items: [grid]
});
});
</script>
</head>
<body></body>
</html>

polydyne
21 Sep 2009, 5:22 AM
Hi,
I was wondering if there is any way I can control the result of calculated field to have certain precision.
I have a cost and adjustment fields, user enters adjustments ( +ve or -ve), calculated field displays a number e.g. 1.32432523523, what can I do to change the decimal precision to lets say 5 digits max..

Thanks for your inputs.
-Polydyne.

Condor
21 Sep 2009, 5:28 AM
Do you really want to round the result or only display a maximum number of digits?

For that you would the toFixed function, e.g.

{header: 'Number', dataIndex: 'mynumber', renderer: function(v){
return v.toFixed ? v.toFixed(5) : v;
}}

madcity
30 Sep 2009, 8:34 PM
I tried both Condor's and mjlecomte's versions and it seems that neither allows for conditions within the calc. Here is an example of what I was trying:


Test.Data.ContactRecord = Ext.ux.data.CalcRecord.create([
{ name : "id" }, { name : "account" }, { name : "first_name" }, { name : "last_name" }, { name : "title" }, { name : "clinic" }, { name : "street" },
{ name : "city" }, { name : "state" }, { name : "zip" }, { name : "office" }, { name : "emergency" }, { name : "fax" }, { name : "notes" },
{ name : "business_id" }, {
name: 'name',
dependencies: ['first_name', 'last_name', 'title'],
notDirty: true,
calc: function(record){

switch (record) {
case (record.get('first_name') != '' && record.get('last_name') != '' && record.get('title') !=''):

return (record.get('last_name') + ', ' + record.get('first_name') + ', ' + record.get('title'));

break;
case ('first_name') && ('last_name'):
return record.get('last_name') + ', ' + record.get('first_name')
break;
default:
console.log((record.get('last_name') + ', ' + record.get('first_name') + ', ' + record.get('title')));
return record.get('last_name')
}
}
}

]);It seems that in this case it always falls through to the default value regardless of the test. Is this not supported?

Thanks

mystix
30 Sep 2009, 8:50 PM
It seems that in this case it always falls through to the default value regardless of the test. Is this not supported?

Thanks

javascript switch statements only support simple value comparisons:
http://www.w3schools.com/jS/js_switch.asp

madcity
30 Sep 2009, 9:00 PM
Whoops. OK, but I tried this with nested if else statements and that didn't seem to work either?

madcity
30 Sep 2009, 9:19 PM
Sorry, this does work. Just tried it again with the code below and it's beautiful! I hate raising a flag for no reason - my apologies.


calc: function(record){

if(record.get('first_name') != '' && record.get('last_name') != '' && record.get('title') !='')
{return (record.get('last_name') + ', ' + record.get('first_name') + ', ' + record.get('title'))}
else if (record.get('first_name') != '' && record.get('last_name') != '')
{return record.get('first_name') != '' && record.get('last_name') != ''}
else if (record.get('last_name') != '')
{return record.get('last_name')}




}

Animal
30 Sep 2009, 11:25 PM
On a point of generally accepted Javascript coding style, "if" statements like that are usually cascaded, not treated as if they were nested:



calc: function(record){

if(record.get('first_name') != '' && record.get('last_name') != '' && record.get('title') !='') {
return (record.get('last_name') + ', ' + record.get('first_name') + ', ' + record.get('title'));
} else if (record.get('first_name') != '' && record.get('last_name') != '') {
return record.get('first_name') != '' && record.get('last_name') != '';
} else if (record.get('last_name') != '') {
return record.get('last_name');
}
}


It's also more understandable when you read it.

madcity
30 Sep 2009, 11:54 PM
Animal,

I totally agree. As you can no doubt tell I am pretty new to this, but I am making progress!

Thanks for your comments.

Thomas Triplet
6 Oct 2009, 1:15 PM
THat's probably a stupid question, but I'm trying to have a renderer for a column that depends on the content of another column. Basically, I'd like the second column to serve as prefix of the 1st one. Having a static prefix is easy, but I dont see how I use the first column as prefix =/

Any suggestion?



var cm = new Ext.grid.ColumnModel( [
{
header: "Prefix",
id: 'prefix',
dataIndex: 'prefix',
editable: true,
editor: new Ext.form.TextField( {
allowBlank: false,
lazyRender: true
})
},
{
header: 'Id',
sortable: true,
id: 'numberID',
dataIndex: 'myid',
renderer: function(v) {
return "prefixFromCol_1_"+v;
},
editor: new Ext.form.NumberField( {
allowNegative: false,
allowBlank: false,
lazyRender: true
})
}
]);



Thanks

Condor
6 Oct 2009, 9:22 PM
The renderer function is called with a lot more parameters (see the ColumnModel.setRenderer API docs).

Thomas Triplet
7 Oct 2009, 8:02 AM
The renderer function is called with a lot more parameters (see the ColumnModel.setRenderer API docs).

That did the trick... my bad, I should have check the documentation more carefully :">
Thanks anyways

Thomas Triplet
8 Oct 2009, 7:27 AM
Was wondering, is it also possible to do something similar with formatting functions that can be used in a template?

Condor
9 Oct 2009, 5:14 AM
Was wondering, is it also possible to do something similar with formatting functions that can be used in a template?

Define 'something similar'. Formatting function in templates are already pretty flexible.

Thomas Triplet
9 Oct 2009, 6:24 AM
I'd like the formatting function to have values from the current record as parameter.

This syntax isn't correct, but in the template, I'd like to have something like that:



var tpl = new Ext.Template([
'Prefix: {column1}',
'Raw Value: {column2}',
'Nicer: {column2:prefixWith({column1})}' // doesn't work =/
]);


tpl.overwrite(Ext.getCmp('detail_panel').body, store.getAt(row).data);

Condor
12 Oct 2009, 3:37 AM
It does when you use an XTemplate, e.g.

var tpl = new Ext.XTemplate([
'Prefix: {column1}',
'Raw Value: {column2}',
'Nicer: {column2:prefixWith(values.column1)}'
]);

davidhw
8 Dec 2009, 1:40 AM
This CalcRecord.js is a gem, many thanks.

dawesi
14 Dec 2009, 6:50 PM
yet another great extension! you're a machine!

Mike Robinson
11 Jan 2010, 10:36 AM
So here's my crack at what I described above. Something I noticed while developing this is that I think the original code for calcFields has a recursive call for set() if notDirty is true for the field. This seems like a waste.

So what I did was to just update the value and then make the field dirty. I don't know that calling set() is needed, or even appropriate on these calculated fields because it would be updating the store to be modified and calling afterEdit on the store (again)....and repeating the calc yet again! (I think this causes exponential loop based on number of fields)

<code omitted>


I'm also looking at this problem, and I also think that what we really have here is "a calculated record,"which is to say, "a record that supports one-or-more calculated fields."

Such a record would need to, operating at the record level perhaps when prompted by the fields, "ask each calculated field to recalculate itself." While iterating through the fields, asking each one to recalculate itself, the record would ignore (recursive) requests made by the fields for the record to recalculate itself.

The trick is, a calculated field would not be included in host-requests. The value of the field is not subject to updates, either by the user or by the host.

It superficially seems to me that this particular bit of code takes this approach . . . Does it?

digitalizarte
1 Sep 2010, 9:45 PM
A variant of the class with some additions and modifications


Ext.namespace('Ext.ux.data');
Ext.ux.data.CalculatedRecord = function (data, id)
{
Ext.ux.data.CalculatedRecord.superclass.constructor.call(this, data, id);
this.calculatedFields();
}
Ext.ux.data.CalculatedRecord.create = function (fields)
{
var f = Ext.extend(Ext.ux.data.CalculatedRecord, {});
var p = f.prototype;
var keyFn = function (field)
{
return field.name;
};
p.fields = new Ext.util.MixedCollection(false, keyFn);
var re = /(\{([^{]*)\})/gi, field;
for (var i = 0, len = fields.length; i < len; i++)
{
field = fields[i];
if (field.tpl)
{
var dep = [];
var replaceFn = function (str, s2, s3)
{
if (dep.indexOf(s3) == -1)
{
dep.push(s3);
}
return "values." + s3;
};
var tpl = "{[" + field.tpl.replace(re, replaceFn) + "]}";
field.calculateTpl = new Ext.XTemplate(tpl, { compiled: true });
field.dependencies = dep;
field.calculate = function (record)
{
return this.calculateTpl.apply(record.data);
};
var oFld = new Ext.data.Field(field);
p.fields.add(oFld);
if (!p.fieldsCalc) p.fieldsCalc = new Ext.util.MixedCollection(false);
p.fieldsCalc.add(oFld);
}
else
{
var oFld = new Ext.data.Field(field);
p.fields.add(oFld);
}
}
var itemFld;
var eachFldDepFn = function (fld)
{
var dependency = fld;
var field = p.fields.get(dependency);
field.dependents = (field.dependents || []).concat([itemFld]);
};
var eachFldCalcFn = function (item)
{
if (item.dependencies)
{
itemFld = item;
Ext.each(item.dependencies, eachFldDepFn, this);
}
};
p.fieldsCalc.each(eachFldCalcFn, this);
f.getField = function (name)
{
return p.fields.get(name);
};
return f;
};
Ext.extend(Ext.ux.data.CalculatedRecord, 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.calculatedFields(name);
var field = this.fields.get(name);
if (field.dependents)
{
var eachDepFn = function (item)
{
if (item.calculate)
{
this.data[item.name] = item.calculate(this);
}
};
Ext.each(field.dependents, eachDepFn, this);
}
if (!this.editing && this.store)
{
this.store.afterEdit(this);
}
},
calculatedFields: function (name)
{
var eachFn = function (field)
{
if ((field.name != name) && (typeof field.calculate == 'function') && (!name || (!field.dependencies || field.dependencies.indexOf(name) != -1)))
{
var value = field.calculate(this);
if (!name || field.notDirty)
{
this.data[field.name] = value;
}
else
{
this.set(field.name, value);
}
}
};
this.fieldsCalc.each(eachFn, this);
}
});

sachin_p84
22 Oct 2010, 2:02 AM
The code mentioned in
http://www.sencha.com/forum/showthread.php?28826-Calculated-fields&p=288628#post288628
does some unnecessary record calculator function calls when its calcMode is 'record'
i have fine-tuned the code as follows :

Note : In this case, call to endEdit() is neccessary after set() operation(s). ( as it is a standard practice).



Ext.namespace('Ext.ux.data');

Ext.ux.data.CalcRecord = function (data, id) {
Ext.ux.data.CalcRecord.superclass.constructor.call(this, data, id);

this.calc();
}

/**
* Generate a constructor for a specific Record layout.
*/
Ext.ux.data.CalcRecord.create = function (o, calc) {
var cfg = Ext.apply({}, calc, {
calcMode: 'fields'
});
var f = Ext.extend(Ext.ux.data.CalcRecord, cfg);
// var f = Ext.extend(Ext.ux.data.CalcRecord, calc || {});
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.calc(name);
//if (!this.editing && this.store) {
// this.store.afterEdit(this);
//}
},
endEdit: function () {
this.calc(name);
this.editing = false;
if (this.dirty) {
this.afterEdit();
}
},
calc: function (name) {
if (this.calcMode == 'fields') {
this.calcFields(name);
} else {
this.calcRecord(name);
}
},
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) {
// do not show calculated field as dirty:
this.data[field.name] = value;
}
else {
this.set(field.name, value);
}
}
}, this);
},
calcRecord: function () {
// get the results
var data = this.calcFn(this.data);

// update the record
if (this.trackDirty) {
for (var name in data) {
this.data[name] = data[name];
if (this.trackDirty.indexOf(name) !== -1) {
// show calculated field as dirty:
if (!this.modified) {
this.modified = {};
}
if (typeof this.modified[name] == 'undefined') {
this.modified[name] = this.data[name];
}
}
}
} else {
Ext.apply(this.data, data);
}
}
});

senchasensei
9 Aug 2013, 12:55 PM
The way that I solved this problem was:

1) For each calculated column (calcField1, calcField2, calcField3, for example) create and associate a corresponding field in the underlying model, with its own convert (http://docs.sencha.com/extjs/4.2.1/#!/api/Ext.data.Field-cfg-convert) function that calculates it based on the record, as desired.

2) In the model, override the set (http://docs.sencha.com/extjs/4.2.1/#!/api/Ext.data.Model-method-set) function as:


set: function() {
this.callParent(arguments);
// callParent for each field with a convert function that isn't directly set by set
this.callParent([{calcField1:null,calcField2:null,calcField3:null}]);
}
I have found this to work flawlessly for my purposes, however I don't claim that this covers all the cases that the original solution covers (though I'm not aware of any discrepancies.)

Just thought this might help someone else.