Author Topic: Issue with Calculations while Copy/Paste  (Read 4995 times)

Sunny

  • Pro Enterprise
  • Jr. Member
  • *
  • Posts: 59
    • View Profile
Issue with Calculations while Copy/Paste
« on: October 15, 2015, 01:45:49 am »
 Hi,

My calculations were not working properly while I am doing copy/paste in my grid (where I use VirtualX and VirtualY as true).

As an example, Lets say my grid has 30 rows with no paging and has vertical scrolling. I have 3 columns which are A,B,C a
A - Discount value (editable field)
B- Original Price (not editable)
C- Calculated based on A and B columns based on the formula : B*(100-A)/100) . Wrote this formula in the column render as a function.

And the top of the grid I display a Summary value which Sum of Column C.

When I copy paste some value (10) in column A , my summary is calculated only with the 10 rows which are visible(rendered) at that time in the grid, and the rest 20 rows are not been counted since they have not been rendered, which made my summary calculation incorrect.

Please advice.

paramvir

  • Administrator
  • Hero Member
  • *****
  • Posts: 6260
    • View Profile
Re: Issue with Calculations while Copy/Paste
« Reply #1 on: October 15, 2015, 06:53:05 am »
Please share a small test case so that the issue can be looked into.
« Last Edit: October 15, 2015, 11:54:09 am by paramquery »

Sunny

  • Pro Enterprise
  • Jr. Member
  • *
  • Posts: 59
    • View Profile
Re: Issue with Calculations while Copy/Paste
« Reply #2 on: October 16, 2015, 01:06:32 am »
Hi,

With little more effort, I am able to provide you test case using one of your demo for AutoSave example at:http://paramquery.com/pro/demos/editing_instant
You can copy paste below code in the example and can notice the behavior. The changes I made to the code are
  • Enabled QuantityPerUnit column and titled as TotalCost, added render function to act as calcualted column
  • Added Refresh call in Change function
  • Added virtualX:true, virtualY:true

You can notice the issue, when you select the 'Units in Stock' cell in the first row, and do Cntrl+Shift+End keys, which will select the whole column and paste (using Cntrl+V) some value. It should re-calculate the 'Total Cost' column now. You can observe in the Firebug console, updateList in the Post request doesn't reflect the changes for the first few rows in 'QuantityPerUnit' column.

This code works fine when I remove virtualX:true and virtualY:true !!!

Code: [Select]

    $(function () {
        var obj = {
            hwrap: false,
            resizable: true,
            rowBorders: false,
            numberCell: { show: true },
            trackModel: { on: true }, //to turn on the track changes.           
            toolbar: {
                items: [
                    { type: 'button', icon: 'ui-icon-plus', label: 'New Product', listener:
                        { "click": function (evt, ui) {
                            //append empty row at the end.                           
                            var rowData = { ProductName: 'new product', UnitPrice: 0.2 }; //empty row
                            var rowIndx = grid.addRow({ rowData: rowData });
                            grid.goToPage({ rowIndx: rowIndx });
                            grid.editFirstCellInRow({ rowIndx: rowIndx });
                        }
                        }
                    },
                    { type: 'separator' },
                    { type: 'button', icon: 'ui-icon-arrowreturn-1-s', label: 'Undo', cls: 'changes', listener:
                        { "click": function (evt, ui) {
                            grid.history({ method: 'undo' });
                        }
                        },
                        options: { disabled: true }
                    },
                    { type: 'button', icon: 'ui-icon-arrowrefresh-1-s', label: 'Redo', listener:
                        { "click": function (evt, ui) {
                            grid.history({ method: 'redo' });
                        }
                        },
                        options: { disabled: true }
                    },
                    {
                        type: "<span class='saving'>Saving...</span>"
                    }
                ]
            },
            scrollModel: {
                autoFit: true
            },
            historyModel: {
                checkEditableAdd: true
            },
            editor: {
                select: true
            },
            title: "<b>Auto save</b>",
            change: function (evt, ui) {

                var grid = this;
                if (grid.isDirty() && grid.isValidChange({allowInvalid: true}).valid) {
    grid.refresh();                           
                    var changes = grid.getChanges({ format: 'byVal' });
                    $.ajax({
                        url: '/pro/products/batch',
                        data: {
                            list: JSON.stringify({
                                updateList: changes.updateList,
                                addList: changes.addList,
                                deleteList: changes.deleteList
                            })
                        },
                        dataType: "json",
                        type: "POST",
                        async: true,
                        beforeSend: function (jqXHR, settings) {
                            $(".saving", grid.widget()).show();
                        },
                        success: function (changes) {
                            //commit the changes.
    grid.commit({ type: 'add', rows: changes.addList });
                            grid.commit({ type: 'update', rows: changes.updateList });
                            grid.commit({ type: 'delete', rows: changes.deleteList });
                        },
                        complete: function () {
                            $(".saving", grid.widget()).hide();
                        }
                    });
                }
            },
            history: function (evt, ui) {
                var $grid = this.widget();
                if (ui.canUndo != null) {
                    $("button.changes", $grid).button("option", { disabled: !ui.canUndo });
                }
                if (ui.canRedo != null) {
                    $("button:contains('Redo')", $grid).button("option", "disabled", !ui.canRedo);
                }
                $("button:contains('Undo')", $grid).button("option", { label: 'Undo (' + ui.num_undo + ')' });
                $("button:contains('Redo')", $grid).button("option", { label: 'Redo (' + ui.num_redo + ')' });
            },
            colModel: [
                { title: "Product ID", dataType: "integer", dataIndx: "ProductID", editable: false, width: 80 },
                { title: "Product Name", width: 165, dataType: "string", dataIndx: "ProductName",
                    validations: [
                        { type: 'minLen', value: 1, msg: "Required" },
                        { type: 'maxLen', value: 40, msg: "length should be <= 40" }
                    ]
                },
                { title: "Total Cost", hidden: false, width: 140, dataType: "string", align: "right", dataIndx: "QuantityPerUnit",
                    render:function(ui){
     ui.rowData.QuantityPerUnit = ui.rowData.UnitPrice * ui.rowData.UnitsInStock;
     ui.cellData = ui.rowData.QuantityPerUnit;
     return ui.cellData;
}
                },
                { title: "Unit Price", width: 100, dataType: "float", align: "right", dataIndx: "UnitPrice",
                    validations: [
                        { type: 'nonEmpty', msg: "Required" },
                        { type: 'gt', value: 0.5, msg: "better be > 0.5", warn: true}],
                    render: function (ui) {
                        var cellData = ui.cellData;
                        if (cellData != null) {
                            return "$" + parseFloat(ui.cellData).toFixed(2);
                        }
                        else {
                            return "";
                        }
                    }
                },
                { title: "Units In Stock", width: 100, dataType: "integer", align: "right", dataIndx: "UnitsInStock",
                    validations: [{ type: 'gte', value: 0, msg: "Required"}]
                },
                { title: "Discontinued", width: 100, dataType: "bool", align: "center", dataIndx: "Discontinued",
                    editor: false,
                    type: 'checkbox',
                    validations: [{ type: 'nonEmpty', msg: "Required"}]
                },
                { title: "", editable: false, minWidth: 85, sortable: false,
                    render: function (ui) {
                        return "<button type='button' class='delete_btn'>Delete</button>";
                    },
                    postRender: function (ui) {
                        var grid = this,
                            $cell = grid.getCell(ui);
                        $cell.find(".delete_btn")
                            .button({ icons: { primary: 'ui-icon-scissors'} })
                            .bind("click", function (evt) {
                                grid.deleteRow({ rowIndx: ui.rowIndx });
                            });
                    }
                }
            ],
            postRenderInterval: -1, //synchronous post render.
            pageModel: { type: "local", rPP: 20 },
virtualX:true,virtualY:true,
            dataModel: {
                dataType: "JSON",
                location: "remote",
                recIndx: "ProductID",
                url: "/pro/products/get", //for ASP.NET
                //url: "/pro/products.php", //for PHP
                getData: function (response) {
                    return { data: response.data };
                }
            },
            load: function (evt, ui) {
                var grid = this,
                    data = grid.option('dataModel').data;

                grid.widget().pqTooltip(); //attach a tooltip.

                //validate the whole data.
                grid.isValid({ data: data });
            }
        };
        var grid = pq.grid("#grid_editing", obj);
    });




paramvir

  • Administrator
  • Hero Member
  • *****
  • Posts: 6260
    • View Profile
Re: Issue with Calculations while Copy/Paste
« Reply #3 on: October 16, 2015, 07:02:12 am »
I see what you mean and that is the expected behavior from render callback.

column.render callback purpose is to provide a cell view of the underlying data and it's called whenever the cell is rendered in grid. In non virtual mode all the cells on a page are rendered at once while in virtual mode, the cells are rendered depending upon the size of viewport ( scrollable area ).
 
So if you are looking for a way to persist the calculations in data (and/or) post it to the server, please use beforeValidate or change event by iterating over the changed rows and update Total Cost in newRow.

To refresh row whenever cell is changed.

Code: [Select]
cellSave: function(evt, ui){
//debugger;
this.refreshRow(ui);
},

and to update the Total cost column.

Code: [Select]
grid.on('change', function(evt, ui){
//debugger;
var rowList = ui.rowList;
for(var i=0;i<rowList.length;i++){
var ro = rowList[i],
rowData = ro.rowData,
QuantityPerUnit = rowData.UnitPrice * rowData.UnitsInStock;
rowData.QuantityPerUnit = isNaN(QuantityPerUnit)? undefined: QuantityPerUnit;
}
});

Final code:

Code: [Select]

    $(function () {
        var obj = {
            hwrap: false,
            resizable: true,
            rowBorders: false,
            numberCell: { show: true },
            trackModel: { on: true }, //to turn on the track changes.           
            toolbar: {
                items: [
                    { type: 'button', icon: 'ui-icon-plus', label: 'New Product', listener:
                        { "click": function (evt, ui) {
                            //append empty row at the end.                           
                            var rowData = { ProductName: 'new product', UnitPrice: 0.2 }; //empty row
                            var rowIndx = grid.addRow({ rowData: rowData });
                            grid.goToPage({ rowIndx: rowIndx });
                            grid.editFirstCellInRow({ rowIndx: rowIndx });
                        }
                        }
                    },
                    { type: 'separator' },
                    { type: 'button', icon: 'ui-icon-arrowreturn-1-s', label: 'Undo', cls: 'changes', listener:
                        { "click": function (evt, ui) {
                            grid.history({ method: 'undo' });
                        }
                        },
                        options: { disabled: true }
                    },
                    { type: 'button', icon: 'ui-icon-arrowrefresh-1-s', label: 'Redo', listener:
                        { "click": function (evt, ui) {
                            grid.history({ method: 'redo' });
                        }
                        },
                        options: { disabled: true }
                    },
                    {
                        type: "<span class='saving'>Saving...</span>"
                    }
                ]
            },
            scrollModel: {
                autoFit: true
            },
            historyModel: {
                checkEditableAdd: true
            },
            editor: {
                select: true
            },
            title: "<b>Auto save</b>",
            change: function (evt, ui) {
//debugger;
                var grid = this;
                if (grid.isDirty() && grid.isValidChange({allowInvalid: true}).valid) {
    //grid.refresh();                           
                    var changes = grid.getChanges({ format: 'byVal' });
                    $.ajax({
                        url: '/pro/products/batch',
                        data: {
                            list: JSON.stringify({
                                updateList: changes.updateList,
                                addList: changes.addList,
                                deleteList: changes.deleteList
                            })
                        },
                        dataType: "json",
                        type: "POST",
                        async: true,
                        beforeSend: function (jqXHR, settings) {
                            $(".saving", grid.widget()).show();
                        },
                        success: function (changes) {
                            //commit the changes.
    grid.commit({ type: 'add', rows: changes.addList });
                            grid.commit({ type: 'update', rows: changes.updateList });
                            grid.commit({ type: 'delete', rows: changes.deleteList });
                        },
                        complete: function () {
                            $(".saving", grid.widget()).hide();
                        }
                    });
                }
            },
            history: function (evt, ui) {
                var $grid = this.widget();
                if (ui.canUndo != null) {
                    $("button.changes", $grid).button("option", { disabled: !ui.canUndo });
                }
                if (ui.canRedo != null) {
                    $("button:contains('Redo')", $grid).button("option", "disabled", !ui.canRedo);
                }
                $("button:contains('Undo')", $grid).button("option", { label: 'Undo (' + ui.num_undo + ')' });
                $("button:contains('Redo')", $grid).button("option", { label: 'Redo (' + ui.num_redo + ')' });
            },
            colModel: [
                { title: "Product ID", dataType: "integer", dataIndx: "ProductID", editable: false, width: 80 },
                { title: "Product Name", width: 165, dataType: "string", dataIndx: "ProductName",
                    validations: [
                        { type: 'minLen', value: 1, msg: "Required" },
                        { type: 'maxLen', value: 40, msg: "length should be <= 40" }
                    ]
                },
                { title: "Total Cost", hidden: false, width: 140, dataType: "string", align: "right", dataIndx: "QuantityPerUnit",
                    render:function(ui){
ui.rowData.QuantityPerUnit = ui.rowData.UnitPrice * ui.rowData.UnitsInStock;
ui.cellData = ui.rowData.QuantityPerUnit;
return ui.cellData;
}
                },
                { title: "Unit Price", width: 100, dataType: "float", align: "right", dataIndx: "UnitPrice",
                    validations: [
                        { type: 'nonEmpty', msg: "Required" },
                        { type: 'gt', value: 0.5, msg: "better be > 0.5", warn: true}],
                    render: function (ui) {
                        var cellData = ui.cellData;
                        if (cellData != null) {
                            return "$" + parseFloat(ui.cellData).toFixed(2);
                        }
                        else {
                            return "";
                        }
                    }
                },
                { title: "Units In Stock", width: 100, dataType: "integer", align: "right", dataIndx: "UnitsInStock",
                    validations: [{ type: 'gte', value: 0, msg: "Required"}]
                },
                { title: "Discontinued", width: 100, dataType: "bool", align: "center", dataIndx: "Discontinued",
                    editor: false,
                    type: 'checkbox',
                    validations: [{ type: 'nonEmpty', msg: "Required"}]
                },
                { title: "", editable: false, minWidth: 85, sortable: false,
                    render: function (ui) {
                        return "<button type='button' class='delete_btn'>Delete</button>";
                    },
                    postRender: function (ui) {
                        var grid = this,
                            $cell = grid.getCell(ui);
                        $cell.find(".delete_btn")
                            .button({ icons: { primary: 'ui-icon-scissors'} })
                            .bind("click", function (evt) {
                                grid.deleteRow({ rowIndx: ui.rowIndx });
                            });
                    }
                }
            ],
            postRenderInterval: -1, //synchronous post render.
            pageModel: { type: "local", rPP: 20 },
virtualX:true,virtualY:true,
            dataModel: {
                dataType: "JSON",
                location: "remote",
                recIndx: "ProductID",
                url: "/pro/products/get", //for ASP.NET
                //url: "/pro/products.php", //for PHP
                getData: function (response) {
                    return { data: response.data };
                }
            },
cellSave: function(evt, ui){
//debugger;
this.refreshRow(ui);
},
            load: function (evt, ui) {
                var grid = this,
                    data = grid.option('dataModel').data;

                grid.widget().pqTooltip(); //attach a tooltip.

                //validate the whole data.
                grid.isValid({ data: data });
            }
        };
        var grid = pq.grid("#grid_editing", obj);
/*use either beforeValidate or change event.
grid.on('beforeValidate', function(evt, ui){
//debugger;
var rowList = ui.rowList;
for(var i=0;i<rowList.length;i++){
var ro = rowList[i],
rowData = ro.rowData,
newRow = ro.newRow,
oldRow = ro.oldRow,
UnitPrice = newRow.UnitPrice === undefined? rowData.UnitPrice: newRow.UnitPrice,
UnitsInStock = newRow.UnitsInStock === undefined? rowData.UnitsInStock: newRow.UnitsInStock,
QuantityPerUnit = UnitPrice * UnitsInStock;
newRow.QuantityPerUnit = isNaN(QuantityPerUnit)? undefined: QuantityPerUnit;
oldRow.QuantityPerUnit = rowData.QuantityPerUnit;
}
});*/
grid.on('change', function(evt, ui){
//debugger;
var rowList = ui.rowList;
for(var i=0;i<rowList.length;i++){
var ro = rowList[i],
rowData = ro.rowData,
QuantityPerUnit = rowData.UnitPrice * rowData.UnitsInStock;
rowData.QuantityPerUnit = isNaN(QuantityPerUnit)? undefined: QuantityPerUnit;
}
});
    });
« Last Edit: October 16, 2015, 04:22:19 pm by paramquery »

Sunny

  • Pro Enterprise
  • Jr. Member
  • *
  • Posts: 59
    • View Profile
Re: Issue with Calculations while Copy/Paste
« Reply #4 on: October 16, 2015, 09:32:18 pm »
Thank you for sharing the code with the solution. I thought of using the change event , but I didn't like the way the code has to repeated twice in render function and also in the change function. As, I have 5 to 6 calculated columns in my grid.

I am not sure, if there is another way or possible to change refreshRow method which should render all the cells in the row including that are not available in the viewport.

paramvir

  • Administrator
  • Hero Member
  • *****
  • Posts: 6260
    • View Profile
Re: Issue with Calculations while Copy/Paste
« Reply #5 on: October 19, 2015, 09:17:05 pm »
You are right, calculations should better not be repeated.

All row computations can be put in a single function computeRow which is called from load event and change event.

And render callback can be removed.

Code: [Select]
function computeRow(rowData){
var QuantityPerUnit = rowData.UnitPrice * rowData.UnitsInStock;
rowData.QuantityPerUnit = isNaN(QuantityPerUnit)? undefined: QuantityPerUnit;
}
grid.on("load", function(){
var grid = this,
                              data = grid.option('dataModel.data');
for(var i=0, len = data.length; i<len; i++){
var rowData = data[i];
computeRow(rowData);
}
});
grid.on('change', function(evt, ui){
//debugger;
var rowList = ui.rowList;
for(var i=0;i<rowList.length;i++){
var ro = rowList[i],
rowData = ro.rowData;
computeRow(rowData);
}
});

cellSave event is still required to ensure that whole row view is refreshed upon cell editing.

Code: [Select]
cellSave: function(evt, ui){
//debugger;
this.refreshRow(ui);
},
« Last Edit: October 19, 2015, 09:19:09 pm by paramquery »

Sunny

  • Pro Enterprise
  • Jr. Member
  • *
  • Posts: 59
    • View Profile
Re: Issue with Calculations while Copy/Paste
« Reply #6 on: October 21, 2015, 08:16:25 pm »
Thank you.