This section shows how rows can be added, removed and updated using the Server-Side Transaction API.
Server-Side Transactions require Row IDs to be supplied to grid.
Transaction API Copy Link
The SSRM Transaction API allows rows to be added, removed or updated in the grid:
Apply transactions to the server side row model. |
When the server-side store has a known last row index, remove transactions only delete rows that are currently in cache. If a delete is repeated or targets a row outside the loaded range, the grid ignores it and keeps the current store size. To explicitly set the new store size, provide rowCount on the transaction.
These operations are shown in the snippet below:
api.applyServerSideTransaction({
add: [
{ tradeId: 101, portfolio: 'Aggressive', product: 'Aluminium', book: 'GL-62472', current: 57969 }
],
update: [
{ tradeId: 102, portfolio: 'Aggressive', product: 'Aluminium', book: 'GL-624723', current: 58927 }
],
remove: [
{ tradeId: 103 }
]
});The following example demonstrates add / update and remove operations via the Server-Side Transaction API. Note the following:
- When clicking any of the buttons, the console logs each transaction as it is applied to the grid.
- Add Above Selected - adds a row above the selected row using the
addIndexproperty as rows are added at the end by default. - Update Selected - updates the 'current' value on the selected row.
- Removed Selected - removes the selected row.
import {
ColDef,
GetRowIdParams,
GridApi,
GridOptions,
GridReadyEvent,
HighlightChangesModule,
IServerSideGetRowsParams,
ModuleRegistry,
ServerSideTransaction,
ServerSideTransactionResult,
createGrid,
enableDevValidations,
} from "ag-grid-community";
import {
ServerSideRowModelApiModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { data } from "./data";
import { FakeServer } from "./fakeServer";
// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
HighlightChangesModule,
ServerSideRowModelModule,
ServerSideRowModelApiModule,
]);
const columnDefs: ColDef[] = [
{ field: "tradeId" },
{ field: "portfolio" },
{ field: "book" },
{ field: "current" },
];
let gridApi: GridApi;
const gridOptions: GridOptions = {
columnDefs,
defaultColDef: {
flex: 1,
minWidth: 100,
enableCellChangeFlash: true,
},
autoGroupColumnDef: {
minWidth: 220,
},
getRowId: (params: GetRowIdParams) => `${params.data.tradeId}`,
onGridReady: (params: GridReadyEvent) => {
// setup the fake server
const server = new FakeServer(data);
// create datasource with a reference to the fake server
const datasource = getServerSideDatasource(server);
// register the datasource with the grid
params.api.setGridOption("serverSideDatasource", datasource);
},
rowSelection: { mode: "singleRow" },
rowModelType: "serverSide",
};
function getServerSideDatasource(server: any) {
return {
getRows: (params: IServerSideGetRowsParams) => {
const response = server.getData(params.request);
// adding delay to simulate real server call
setTimeout(() => {
if (response.success) {
// call the success callback
params.success({
rowData: response.rows,
rowCount: response.lastRow,
});
} else {
// inform the grid request failed
params.fail();
}
}, 300);
},
};
}
function addRow() {
const selectedRows = gridApi!.getSelectedNodes();
if (selectedRows.length === 0) {
console.log("[Example] No row selected.");
return;
}
const rowIndex = selectedRows[0].rowIndex;
const transaction: ServerSideTransaction = {
addIndex: rowIndex != null ? rowIndex : undefined,
add: [createRow()],
};
const result = gridApi!.applyServerSideTransaction(transaction);
logResults(transaction, result);
}
function updateRow() {
const selectedRows = gridApi!.getSelectedNodes();
if (selectedRows.length === 0) {
console.log("[Example] No row selected.");
return;
}
const transaction: ServerSideTransaction = {
update: [{ ...selectedRows[0].data, current: getNewValue() }],
};
const result = gridApi!.applyServerSideTransaction(transaction);
logResults(transaction, result);
}
function removeRow() {
const selectedRows = gridApi!.getSelectedNodes();
if (selectedRows.length === 0) {
console.log("[Example] No row selected.");
return;
}
const transaction: ServerSideTransaction = { remove: [selectedRows[0].data] };
const result = gridApi!.applyServerSideTransaction(transaction);
logResults(transaction, result);
}
function logResults(
transaction: ServerSideTransaction,
result?: ServerSideTransactionResult,
) {
console.log(
"[Example] - Applied transaction:",
transaction,
"Result:",
result,
);
}
function getNewValue() {
return Math.floor(window.agRandom() * 100000) + 100;
}
let serverCurrentTradeId = data.length;
function createRow() {
return {
portfolio: "Aggressive",
product: "Aluminium",
book: "GL-62472",
tradeId: ++serverCurrentTradeId,
current: getNewValue(),
};
}
const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).addRow = addRow;
(<any>window).updateRow = updateRow;
(<any>window).removeRow = removeRow;
}
.example-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
#myGrid {
flex: 1 1 0px;
width: 100%;
}
export const data = [
{
product: 'Palm Oil',
portfolio: 'Aggressive',
book: 'GL-62472',
current: 47219,
tradeId: 0,
},
{
product: 'Palm Oil',
portfolio: 'Aggressive',
book: 'GL-62472',
current: 84157,
tradeId: 1,
},
{
product: 'Palm Oil',
portfolio: 'Aggressive',
book: 'GL-62472',
current: 50829,
tradeId: 2,
},
{
product: 'Palm Oil',
portfolio: 'Aggressive',
book: 'GL-62473',
current: 23098,
tradeId: 3,
},
{
product: 'Palm Oil',
portfolio: 'Aggressive',
book: 'GL-62473',
current: 4678,
tradeId: 4,
},
{
product: 'Palm Oil',
portfolio: 'Aggressive',
book: 'GL-62473',
current: 51441,
tradeId: 5,
},
{
product: 'Palm Oil',
portfolio: 'Aggressive',
book: 'GL-62474',
current: 5966,
tradeId: 6,
},
{
product: 'Palm Oil',
portfolio: 'Aggressive',
book: 'GL-62474',
current: 11995,
tradeId: 7,
},
{
product: 'Palm Oil',
portfolio: 'Aggressive',
book: 'GL-62474',
current: 15026,
tradeId: 8,
},
{
product: 'Palm Oil',
portfolio: 'Aggressive',
book: 'GL-62474',
current: 84306,
tradeId: 9,
},
{
product: 'Palm Oil',
portfolio: 'Defensive',
book: 'GL-62475',
current: 50343,
tradeId: 10,
},
{
product: 'Palm Oil',
portfolio: 'Defensive',
book: 'GL-62475',
current: 56278,
tradeId: 11,
},
{
product: 'Palm Oil',
portfolio: 'Defensive',
book: 'GL-62476',
current: 68880,
tradeId: 12,
},
{
product: 'Palm Oil',
portfolio: 'Defensive',
book: 'GL-62476',
current: 90513,
tradeId: 13,
},
{
product: 'Palm Oil',
portfolio: 'Defensive',
book: 'GL-62476',
current: 49321,
tradeId: 14,
},
{
product: 'Palm Oil',
portfolio: 'Defensive',
book: 'GL-62476',
current: 60558,
tradeId: 15,
},
{
product: 'Palm Oil',
portfolio: 'Income',
book: 'GL-62477',
current: 88868,
tradeId: 16,
},
{
product: 'Palm Oil',
portfolio: 'Income',
book: 'GL-62477',
current: 19948,
tradeId: 17,
},
{
product: 'Palm Oil',
portfolio: 'Income',
book: 'GL-62477',
current: 58216,
tradeId: 18,
},
{
product: 'Palm Oil',
portfolio: 'Income',
book: 'GL-62478',
current: 82786,
tradeId: 19,
},
{
product: 'Palm Oil',
portfolio: 'Income',
book: 'GL-62478',
current: 88014,
tradeId: 20,
},
{
product: 'Palm Oil',
portfolio: 'Income',
book: 'GL-62478',
current: 92169,
tradeId: 21,
},
{
product: 'Palm Oil',
portfolio: 'Income',
book: 'GL-62478',
current: 90090,
tradeId: 22,
},
{
product: 'Palm Oil',
portfolio: 'Income',
book: 'GL-62479',
current: 72012,
tradeId: 23,
},
{
product: 'Palm Oil',
portfolio: 'Income',
book: 'GL-62479',
current: 18826,
tradeId: 24,
},
{
product: 'Palm Oil',
portfolio: 'Income',
book: 'GL-62479',
current: 2792,
tradeId: 25,
},
{
product: 'Palm Oil',
portfolio: 'Income',
book: 'GL-62479',
current: 12529,
tradeId: 26,
},
{
product: 'Palm Oil',
portfolio: 'Income',
book: 'GL-62480',
current: 47307,
tradeId: 27,
},
{
product: 'Palm Oil',
portfolio: 'Income',
book: 'GL-62480',
current: 62488,
tradeId: 28,
},
{
product: 'Palm Oil',
portfolio: 'Income',
book: 'GL-62480',
current: 97181,
tradeId: 29,
},
{
product: 'Palm Oil',
portfolio: 'Speculative',
book: 'GL-62481',
current: 96506,
tradeId: 30,
},
{
product: 'Palm Oil',
portfolio: 'Speculative',
book: 'GL-62481',
current: 87164,
tradeId: 31,
},
{
product: 'Palm Oil',
portfolio: 'Speculative',
book: 'GL-62481',
current: 87652,
tradeId: 32,
},
{
product: 'Palm Oil',
portfolio: 'Speculative',
book: 'GL-62481',
current: 65921,
tradeId: 33,
},
{
product: 'Palm Oil',
portfolio: 'Speculative',
book: 'GL-62482',
current: 74646,
tradeId: 34,
},
{
product: 'Palm Oil',
portfolio: 'Speculative',
book: 'GL-62482',
current: 72141,
tradeId: 35,
},
{
product: 'Palm Oil',
portfolio: 'Speculative',
book: 'GL-62482',
current: 27790,
tradeId: 36,
},
{
product: 'Palm Oil',
portfolio: 'Speculative',
book: 'GL-62483',
current: 16450,
tradeId: 37,
},
{
product: 'Palm Oil',
portfolio: 'Speculative',
book: 'GL-62483',
current: 73037,
tradeId: 38,
},
{
product: 'Palm Oil',
portfolio: 'Speculative',
book: 'GL-62484',
current: 86713,
tradeId: 39,
},
{
product: 'Palm Oil',
portfolio: 'Speculative',
book: 'GL-62484',
current: 84399,
tradeId: 40,
},
{
product: 'Palm Oil',
portfolio: 'Speculative',
book: 'GL-62484',
current: 31984,
tradeId: 41,
},
{
product: 'Palm Oil',
portfolio: 'Speculative',
book: 'GL-62484',
current: 96176,
tradeId: 42,
},
{
product: 'Palm Oil',
portfolio: 'Hybrid',
book: 'GL-62485',
current: 86310,
tradeId: 43,
},
{
product: 'Palm Oil',
portfolio: 'Hybrid',
book: 'GL-62485',
current: 20916,
tradeId: 44,
},
{
product: 'Palm Oil',
portfolio: 'Hybrid',
book: 'GL-62485',
current: 14266,
tradeId: 45,
},
{
product: 'Palm Oil',
portfolio: 'Hybrid',
book: 'GL-62486',
current: 8799,
tradeId: 46,
},
{
product: 'Palm Oil',
portfolio: 'Hybrid',
book: 'GL-62486',
current: 11523,
tradeId: 47,
},
{
product: 'Rubber',
portfolio: 'Aggressive',
book: 'GL-62487',
current: 31334,
tradeId: 48,
},
{
product: 'Rubber',
portfolio: 'Aggressive',
book: 'GL-62487',
current: 96971,
tradeId: 49,
},
{
product: 'Rubber',
portfolio: 'Aggressive',
book: 'GL-62487',
current: 62525,
tradeId: 50,
},
{
product: 'Rubber',
portfolio: 'Aggressive',
book: 'GL-62487',
current: 4083,
tradeId: 51,
},
{
product: 'Rubber',
portfolio: 'Aggressive',
book: 'GL-62488',
current: 94926,
tradeId: 52,
},
{
product: 'Rubber',
portfolio: 'Aggressive',
book: 'GL-62488',
current: 27236,
tradeId: 53,
},
{
product: 'Rubber',
portfolio: 'Aggressive',
book: 'GL-62489',
current: 86257,
tradeId: 54,
},
{
product: 'Rubber',
portfolio: 'Aggressive',
book: 'GL-62489',
current: 98446,
tradeId: 55,
},
{
product: 'Rubber',
portfolio: 'Aggressive',
book: 'GL-62489',
current: 40089,
tradeId: 56,
},
{
product: 'Rubber',
portfolio: 'Aggressive',
book: 'GL-62490',
current: 11542,
tradeId: 57,
},
{
product: 'Rubber',
portfolio: 'Aggressive',
book: 'GL-62490',
current: 7186,
tradeId: 58,
},
{
product: 'Rubber',
portfolio: 'Aggressive',
book: 'GL-62490',
current: 11326,
tradeId: 59,
},
{
product: 'Rubber',
portfolio: 'Aggressive',
book: 'GL-62490',
current: 10429,
tradeId: 60,
},
{
product: 'Rubber',
portfolio: 'Defensive',
book: 'GL-62491',
current: 29595,
tradeId: 61,
},
{
product: 'Rubber',
portfolio: 'Defensive',
book: 'GL-62491',
current: 33256,
tradeId: 62,
},
{
product: 'Rubber',
portfolio: 'Defensive',
book: 'GL-62491',
current: 70631,
tradeId: 63,
},
{
product: 'Rubber',
portfolio: 'Defensive',
book: 'GL-62491',
current: 74711,
tradeId: 64,
},
{
product: 'Rubber',
portfolio: 'Defensive',
book: 'GL-62492',
current: 27317,
tradeId: 65,
},
{
product: 'Rubber',
portfolio: 'Defensive',
book: 'GL-62492',
current: 62930,
tradeId: 66,
},
{
product: 'Rubber',
portfolio: 'Defensive',
book: 'GL-62492',
current: 34468,
tradeId: 67,
},
{
product: 'Rubber',
portfolio: 'Defensive',
book: 'GL-62492',
current: 18429,
tradeId: 68,
},
{
product: 'Rubber',
portfolio: 'Defensive',
book: 'GL-62493',
current: 21450,
tradeId: 69,
},
{
product: 'Rubber',
portfolio: 'Defensive',
book: 'GL-62493',
current: 42759,
tradeId: 70,
},
{
product: 'Rubber',
portfolio: 'Defensive',
book: 'GL-62493',
current: 29336,
tradeId: 71,
},
{
product: 'Rubber',
portfolio: 'Defensive',
book: 'GL-62494',
current: 58534,
tradeId: 72,
},
{
product: 'Rubber',
portfolio: 'Defensive',
book: 'GL-62494',
current: 16575,
tradeId: 73,
},
{
product: 'Rubber',
portfolio: 'Income',
book: 'GL-62495',
current: 26683,
tradeId: 74,
},
{
product: 'Rubber',
portfolio: 'Income',
book: 'GL-62495',
current: 40622,
tradeId: 75,
},
{
product: 'Rubber',
portfolio: 'Income',
book: 'GL-62495',
current: 63484,
tradeId: 76,
},
{
product: 'Rubber',
portfolio: 'Income',
book: 'GL-62496',
current: 76283,
tradeId: 77,
},
{
product: 'Rubber',
portfolio: 'Income',
book: 'GL-62496',
current: 10770,
tradeId: 78,
},
{
product: 'Rubber',
portfolio: 'Income',
book: 'GL-62496',
current: 41974,
tradeId: 79,
},
{
product: 'Rubber',
portfolio: 'Income',
book: 'GL-62496',
current: 25978,
tradeId: 80,
},
{
product: 'Rubber',
portfolio: 'Income',
book: 'GL-62497',
current: 57085,
tradeId: 81,
},
{
product: 'Rubber',
portfolio: 'Income',
book: 'GL-62497',
current: 80855,
tradeId: 82,
},
{
product: 'Rubber',
portfolio: 'Income',
book: 'GL-62498',
current: 68127,
tradeId: 83,
},
{
product: 'Rubber',
portfolio: 'Income',
book: 'GL-62498',
current: 34155,
tradeId: 84,
},
{
product: 'Rubber',
portfolio: 'Speculative',
book: 'GL-62499',
current: 71129,
tradeId: 85,
},
{
product: 'Rubber',
portfolio: 'Speculative',
book: 'GL-62499',
current: 12205,
tradeId: 86,
},
{
product: 'Rubber',
portfolio: 'Speculative',
book: 'GL-62500',
current: 58232,
tradeId: 87,
},
{
product: 'Rubber',
portfolio: 'Speculative',
book: 'GL-62500',
current: 89594,
tradeId: 88,
},
{
product: 'Rubber',
portfolio: 'Speculative',
book: 'GL-62501',
current: 64759,
tradeId: 89,
},
{
product: 'Rubber',
portfolio: 'Speculative',
book: 'GL-62501',
current: 23025,
tradeId: 90,
},
{
product: 'Rubber',
portfolio: 'Speculative',
book: 'GL-62501',
current: 90050,
tradeId: 91,
},
{
product: 'Rubber',
portfolio: 'Speculative',
book: 'GL-62502',
current: 57201,
tradeId: 92,
},
{
product: 'Rubber',
portfolio: 'Speculative',
book: 'GL-62502',
current: 36692,
tradeId: 93,
},
{
product: 'Rubber',
portfolio: 'Speculative',
book: 'GL-62502',
current: 79330,
tradeId: 94,
},
{
product: 'Rubber',
portfolio: 'Hybrid',
book: 'GL-62503',
current: 55489,
tradeId: 95,
},
{
product: 'Rubber',
portfolio: 'Hybrid',
book: 'GL-62503',
current: 12180,
tradeId: 96,
},
{
product: 'Rubber',
portfolio: 'Hybrid',
book: 'GL-62503',
current: 95857,
tradeId: 97,
},
{
product: 'Rubber',
portfolio: 'Hybrid',
book: 'GL-62504',
current: 96432,
tradeId: 98,
},
{
product: 'Rubber',
portfolio: 'Hybrid',
book: 'GL-62504',
current: 68686,
tradeId: 99,
},
{
product: 'Wool',
portfolio: 'Aggressive',
book: 'GL-62505',
current: 36600,
tradeId: 100,
},
{
product: 'Wool',
portfolio: 'Aggressive',
book: 'GL-62505',
current: 70421,
tradeId: 101,
},
{
product: 'Wool',
portfolio: 'Aggressive',
book: 'GL-62506',
current: 69999,
tradeId: 102,
},
{
product: 'Wool',
portfolio: 'Aggressive',
book: 'GL-62506',
current: 89403,
tradeId: 103,
},
{
product: 'Wool',
portfolio: 'Defensive',
book: 'GL-62507',
current: 90423,
tradeId: 104,
},
{
product: 'Wool',
portfolio: 'Defensive',
book: 'GL-62507',
current: 96242,
tradeId: 105,
},
{
product: 'Wool',
portfolio: 'Defensive',
book: 'GL-62508',
current: 85107,
tradeId: 106,
},
{
product: 'Wool',
portfolio: 'Defensive',
book: 'GL-62508',
current: 89268,
tradeId: 107,
},
{
product: 'Wool',
portfolio: 'Defensive',
book: 'GL-62508',
current: 35046,
tradeId: 108,
},
{
product: 'Wool',
portfolio: 'Defensive',
book: 'GL-62509',
current: 6289,
tradeId: 109,
},
{
product: 'Wool',
portfolio: 'Defensive',
book: 'GL-62509',
current: 8184,
tradeId: 110,
},
{
product: 'Wool',
portfolio: 'Income',
book: 'GL-62510',
current: 23909,
tradeId: 111,
},
{
product: 'Wool',
portfolio: 'Income',
book: 'GL-62510',
current: 21300,
tradeId: 112,
},
{
product: 'Wool',
portfolio: 'Income',
book: 'GL-62510',
current: 1876,
tradeId: 113,
},
{
product: 'Wool',
portfolio: 'Income',
book: 'GL-62511',
current: 13551,
tradeId: 114,
},
{
product: 'Wool',
portfolio: 'Income',
book: 'GL-62511',
current: 96455,
tradeId: 115,
},
{
product: 'Wool',
portfolio: 'Income',
book: 'GL-62512',
current: 6303,
tradeId: 116,
},
{
product: 'Wool',
portfolio: 'Income',
book: 'GL-62512',
current: 34072,
tradeId: 117,
},
{
product: 'Wool',
portfolio: 'Income',
book: 'GL-62512',
current: 95427,
tradeId: 118,
},
{
product: 'Wool',
portfolio: 'Speculative',
book: 'GL-62513',
current: 70487,
tradeId: 119,
},
{
product: 'Wool',
portfolio: 'Speculative',
book: 'GL-62513',
current: 59171,
tradeId: 120,
},
{
product: 'Wool',
portfolio: 'Speculative',
book: 'GL-62513',
current: 61667,
tradeId: 121,
},
{
product: 'Wool',
portfolio: 'Speculative',
book: 'GL-62513',
current: 16014,
tradeId: 122,
},
{
product: 'Wool',
portfolio: 'Speculative',
book: 'GL-62514',
current: 6961,
tradeId: 123,
},
{
product: 'Wool',
portfolio: 'Speculative',
book: 'GL-62514',
current: 17369,
tradeId: 124,
},
{
product: 'Wool',
portfolio: 'Hybrid',
book: 'GL-62515',
current: 69849,
tradeId: 125,
},
{
product: 'Wool',
portfolio: 'Hybrid',
book: 'GL-62515',
current: 88171,
tradeId: 126,
},
{
product: 'Wool',
portfolio: 'Hybrid',
book: 'GL-62515',
current: 956,
tradeId: 127,
},
{
product: 'Wool',
portfolio: 'Hybrid',
book: 'GL-62516',
current: 66088,
tradeId: 128,
},
{
product: 'Wool',
portfolio: 'Hybrid',
book: 'GL-62516',
current: 69737,
tradeId: 129,
},
{
product: 'Wool',
portfolio: 'Hybrid',
book: 'GL-62516',
current: 14140,
tradeId: 130,
},
{
product: 'Wool',
portfolio: 'Hybrid',
book: 'GL-62516',
current: 61347,
tradeId: 131,
},
{
product: 'Wool',
portfolio: 'Hybrid',
book: 'GL-62517',
current: 29209,
tradeId: 132,
},
{
product: 'Wool',
portfolio: 'Hybrid',
book: 'GL-62517',
current: 98225,
tradeId: 133,
},
{
product: 'Wool',
portfolio: 'Hybrid',
book: 'GL-62517',
current: 79080,
tradeId: 134,
},
{
product: 'Wool',
portfolio: 'Hybrid',
book: 'GL-62518',
current: 28606,
tradeId: 135,
},
{
product: 'Wool',
portfolio: 'Hybrid',
book: 'GL-62518',
current: 1865,
tradeId: 136,
},
{
product: 'Wool',
portfolio: 'Hybrid',
book: 'GL-62518',
current: 9102,
tradeId: 137,
},
{
product: 'Wool',
portfolio: 'Hybrid',
book: 'GL-62518',
current: 91174,
tradeId: 138,
},
{
product: 'Amber',
portfolio: 'Aggressive',
book: 'GL-62519',
current: 31399,
tradeId: 139,
},
{
product: 'Amber',
portfolio: 'Aggressive',
book: 'GL-62519',
current: 38112,
tradeId: 140,
},
{
product: 'Amber',
portfolio: 'Aggressive',
book: 'GL-62519',
current: 50578,
tradeId: 141,
},
{
product: 'Amber',
portfolio: 'Aggressive',
book: 'GL-62519',
current: 51407,
tradeId: 142,
},
{
product: 'Amber',
portfolio: 'Aggressive',
book: 'GL-62520',
current: 59701,
tradeId: 143,
},
{
product: 'Amber',
portfolio: 'Aggressive',
book: 'GL-62520',
current: 84948,
tradeId: 144,
},
{
product: 'Amber',
portfolio: 'Aggressive',
book: 'GL-62521',
current: 27681,
tradeId: 145,
},
{
product: 'Amber',
portfolio: 'Aggressive',
book: 'GL-62521',
current: 83581,
tradeId: 146,
},
{
product: 'Amber',
portfolio: 'Aggressive',
book: 'GL-62521',
current: 48936,
tradeId: 147,
},
{
product: 'Amber',
portfolio: 'Aggressive',
book: 'GL-62522',
current: 92374,
tradeId: 148,
},
{
product: 'Amber',
portfolio: 'Aggressive',
book: 'GL-62522',
current: 28837,
tradeId: 149,
},
{
product: 'Amber',
portfolio: 'Aggressive',
book: 'GL-62522',
current: 8181,
tradeId: 150,
},
{
product: 'Amber',
portfolio: 'Defensive',
book: 'GL-62523',
current: 1393,
tradeId: 151,
},
{
product: 'Amber',
portfolio: 'Defensive',
book: 'GL-62523',
current: 15208,
tradeId: 152,
},
{
product: 'Amber',
portfolio: 'Defensive',
book: 'GL-62524',
current: 40942,
tradeId: 153,
},
{
product: 'Amber',
portfolio: 'Defensive',
book: 'GL-62524',
current: 66463,
tradeId: 154,
},
{
product: 'Amber',
portfolio: 'Defensive',
book: 'GL-62524',
current: 42318,
tradeId: 155,
},
{
product: 'Amber',
portfolio: 'Defensive',
book: 'GL-62524',
current: 78499,
tradeId: 156,
},
{
product: 'Amber',
portfolio: 'Income',
book: 'GL-62525',
current: 15803,
tradeId: 157,
},
{
product: 'Amber',
portfolio: 'Income',
book: 'GL-62525',
current: 90189,
tradeId: 158,
},
{
product: 'Amber',
portfolio: 'Income',
book: 'GL-62526',
current: 11151,
tradeId: 159,
},
{
product: 'Amber',
portfolio: 'Income',
book: 'GL-62526',
current: 44348,
tradeId: 160,
},
{
product: 'Amber',
portfolio: 'Income',
book: 'GL-62526',
current: 69034,
tradeId: 161,
},
{
product: 'Amber',
portfolio: 'Income',
book: 'GL-62526',
current: 29013,
tradeId: 162,
},
{
product: 'Amber',
portfolio: 'Income',
book: 'GL-62527',
current: 51021,
tradeId: 163,
},
{
product: 'Amber',
portfolio: 'Income',
book: 'GL-62527',
current: 5218,
tradeId: 164,
},
{
product: 'Amber',
portfolio: 'Income',
book: 'GL-62527',
current: 92277,
tradeId: 165,
},
{
product: 'Amber',
portfolio: 'Income',
book: 'GL-62527',
current: 67888,
tradeId: 166,
},
{
product: 'Amber',
portfolio: 'Speculative',
book: 'GL-62528',
current: 15627,
tradeId: 167,
},
{
product: 'Amber',
portfolio: 'Speculative',
book: 'GL-62528',
current: 40676,
tradeId: 168,
},
{
product: 'Amber',
portfolio: 'Speculative',
book: 'GL-62528',
current: 90009,
tradeId: 169,
},
{
product: 'Amber',
portfolio: 'Speculative',
book: 'GL-62528',
current: 43977,
tradeId: 170,
},
{
product: 'Amber',
portfolio: 'Speculative',
book: 'GL-62529',
current: 71031,
tradeId: 171,
},
{
product: 'Amber',
portfolio: 'Speculative',
book: 'GL-62529',
current: 5553,
tradeId: 172,
},
{
product: 'Amber',
portfolio: 'Speculative',
book: 'GL-62529',
current: 74742,
tradeId: 173,
},
{
product: 'Amber',
portfolio: 'Speculative',
book: 'GL-62530',
current: 98007,
tradeId: 174,
},
{
product: 'Amber',
portfolio: 'Speculative',
book: 'GL-62530',
current: 95909,
tradeId: 175,
},
{
product: 'Amber',
portfolio: 'Speculative',
book: 'GL-62531',
current: 65144,
tradeId: 176,
},
{
product: 'Amber',
portfolio: 'Speculative',
book: 'GL-62531',
current: 89147,
tradeId: 177,
},
{
product: 'Amber',
portfolio: 'Hybrid',
book: 'GL-62532',
current: 44434,
tradeId: 178,
},
{
product: 'Amber',
portfolio: 'Hybrid',
book: 'GL-62532',
current: 88236,
tradeId: 179,
},
{
product: 'Amber',
portfolio: 'Hybrid',
book: 'GL-62532',
current: 41887,
tradeId: 180,
},
{
product: 'Amber',
portfolio: 'Hybrid',
book: 'GL-62532',
current: 13171,
tradeId: 181,
},
{
product: 'Amber',
portfolio: 'Hybrid',
book: 'GL-62533',
current: 99986,
tradeId: 182,
},
{
product: 'Amber',
portfolio: 'Hybrid',
book: 'GL-62533',
current: 19674,
tradeId: 183,
},
{
product: 'Amber',
portfolio: 'Hybrid',
book: 'GL-62533',
current: 4643,
tradeId: 184,
},
{
product: 'Amber',
portfolio: 'Hybrid',
book: 'GL-62533',
current: 22252,
tradeId: 185,
},
{
product: 'Amber',
portfolio: 'Hybrid',
book: 'GL-62534',
current: 49758,
tradeId: 186,
},
{
product: 'Amber',
portfolio: 'Hybrid',
book: 'GL-62534',
current: 18079,
tradeId: 187,
},
{
product: 'Amber',
portfolio: 'Hybrid',
book: 'GL-62534',
current: 80386,
tradeId: 188,
},
{
product: 'Amber',
portfolio: 'Hybrid',
book: 'GL-62535',
current: 87401,
tradeId: 189,
},
{
product: 'Amber',
portfolio: 'Hybrid',
book: 'GL-62535',
current: 47481,
tradeId: 190,
},
{
product: 'Copper',
portfolio: 'Aggressive',
book: 'GL-62536',
current: 51641,
tradeId: 191,
},
{
product: 'Copper',
portfolio: 'Aggressive',
book: 'GL-62536',
current: 91643,
tradeId: 192,
},
{
product: 'Copper',
portfolio: 'Aggressive',
book: 'GL-62536',
current: 62642,
tradeId: 193,
},
{
product: 'Copper',
portfolio: 'Aggressive',
book: 'GL-62536',
current: 29963,
tradeId: 194,
},
{
product: 'Copper',
portfolio: 'Aggressive',
book: 'GL-62537',
current: 33693,
tradeId: 195,
},
{
product: 'Copper',
portfolio: 'Aggressive',
book: 'GL-62537',
current: 37410,
tradeId: 196,
},
{
product: 'Copper',
portfolio: 'Aggressive',
book: 'GL-62537',
current: 51435,
tradeId: 197,
},
{
product: 'Copper',
portfolio: 'Aggressive',
book: 'GL-62537',
current: 83191,
tradeId: 198,
},
{
product: 'Copper',
portfolio: 'Aggressive',
book: 'GL-62538',
current: 31159,
tradeId: 199,
},
{
product: 'Copper',
portfolio: 'Aggressive',
book: 'GL-62538',
current: 99437,
tradeId: 200,
},
{
product: 'Copper',
portfolio: 'Aggressive',
book: 'GL-62538',
current: 64024,
tradeId: 201,
},
{
product: 'Copper',
portfolio: 'Aggressive',
book: 'GL-62538',
current: 38069,
tradeId: 202,
},
{
product: 'Copper',
portfolio: 'Defensive',
book: 'GL-62539',
current: 33796,
tradeId: 203,
},
{
product: 'Copper',
portfolio: 'Defensive',
book: 'GL-62539',
current: 87465,
tradeId: 204,
},
{
product: 'Copper',
portfolio: 'Defensive',
book: 'GL-62540',
current: 33352,
tradeId: 205,
},
{
product: 'Copper',
portfolio: 'Defensive',
book: 'GL-62540',
current: 21622,
tradeId: 206,
},
{
product: 'Copper',
portfolio: 'Defensive',
book: 'GL-62541',
current: 4681,
tradeId: 207,
},
{
product: 'Copper',
portfolio: 'Defensive',
book: 'GL-62541',
current: 93152,
tradeId: 208,
},
{
product: 'Copper',
portfolio: 'Defensive',
book: 'GL-62542',
current: 77770,
tradeId: 209,
},
{
product: 'Copper',
portfolio: 'Defensive',
book: 'GL-62542',
current: 9211,
tradeId: 210,
},
{
product: 'Copper',
portfolio: 'Defensive',
book: 'GL-62542',
current: 55437,
tradeId: 211,
},
{
product: 'Copper',
portfolio: 'Defensive',
book: 'GL-62542',
current: 43841,
tradeId: 212,
},
{
product: 'Copper',
portfolio: 'Income',
book: 'GL-62543',
current: 62154,
tradeId: 213,
},
{
product: 'Copper',
portfolio: 'Income',
book: 'GL-62543',
current: 54932,
tradeId: 214,
},
{
product: 'Copper',
portfolio: 'Income',
book: 'GL-62544',
current: 78198,
tradeId: 215,
},
{
product: 'Copper',
portfolio: 'Income',
book: 'GL-62544',
current: 67060,
tradeId: 216,
},
{
product: 'Copper',
portfolio: 'Income',
book: 'GL-62544',
current: 94615,
tradeId: 217,
},
];
export function FakeServer(data) {
alasql.options.cache = false;
return {
getData: function (request) {
const results = executeQuery(request);
return {
success: true,
rows: results,
};
},
};
function executeQuery(request) {
const groupByResult = executeRowGroupQuery(request);
return groupByResult;
}
function executeRowGroupQuery(request) {
const groupByQuery = buildGroupBySql(request);
console.log('[FakeServer] - about to execute row group query:', groupByQuery);
return alasql(groupByQuery, [data]);
}
function buildGroupBySql(request) {
return (
selectSql(request) +
' FROM ?' +
whereSql(request) +
groupBySql(request) +
orderBySql(request) +
limitSql(request)
);
}
function selectSql(request) {
const rowGroupCols = request.rowGroupCols;
const valueCols = request.valueCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const colsToSelect = [rowGroupCol.id];
valueCols.forEach(function (valueCol) {
colsToSelect.push(valueCol.aggFunc + '(' + valueCol.id + ') AS ' + valueCol.id);
});
return 'SELECT ' + colsToSelect.join(', ');
}
return 'SELECT *';
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
function groupBySql(request) {
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
return ' GROUP BY ' + rowGroupCol.id + ' HAVING count(*) > 0';
}
return '';
}
function orderBySql(request) {
const sortModel = request.sortModel;
if (sortModel.length === 0) return '';
const sorts = sortModel.map(function (s) {
return s.colId + ' ' + s.sort.toUpperCase();
});
return ' ORDER BY ' + sorts.join(', ');
}
function limitSql(request) {
if (request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level
return rowGroupCols.length > groupKeys.length;
}
function getLastRowIndex(request, results) {
if (!results || results.length === 0) {
return null;
}
if (request.endRow == undefined || request.startRow == undefined) {
return results.length;
}
const currentLastRow = request.startRow + results.length;
return currentLastRow <= request.endRow ? currentLastRow : -1;
}
}
// IE Workaround - as templates literals are not supported
function interpolate(str, o) {
return str.replace(/{([^{}]*)}/g, function (a, b) {
const r = o[b];
return typeof r === 'string' || typeof r === 'number' ? r : a;
});
}
<div class="example-wrapper">
<div style="margin-bottom: 5px">
<button onClick="addRow()">Add Above Selected</button>
<button onClick="updateRow()">Update Selected</button>
<button onClick="removeRow()">Remove Selected</button>
</div>
<div id="myGrid"></div>
</div>
Row Grouping Copy Link
To use transactions while using row grouping, transactions need to be applied to the specific row group. This is done by providing a route when applying the transaction. It is also necessary to inform the grid when group rows are updated, added or removed.
The snippet below demonstrates creating a group row transaction for rows which are the first of their group, as the leaf rows will be requested via getRows when the group is expanded.
// create the group row at the root level (only if it's the first row for this group)
api.applyServerSideTransaction({
route: [],
add: [{ portfolio: 'Aggressive' }]
});
// otherwise, create the leaf node inside of the 'Aggressive' group
api.applyServerSideTransaction({
route: ['Aggressive'],
add: [row]
});In the example below, note the following:
- When clicking any of the buttons, the console logs each transaction as it is applied to the grid.
- To add a new row, if the group didn't previously exist, then the route is omitted and the group row is added. If it did previously exist, then the group route is provided and the leaf node is added.
- To delete a row, if the group row would be deleted then a transaction needs to be applied to remove this group row instead of the leaf row.
- To move a row between groups, the row needs to be deleted from the old group with one transaction, and added to the new group with another.
import {
ColDef,
GetRowIdParams,
GridApi,
GridOptions,
GridReadyEvent,
HighlightChangesModule,
IServerSideGetRowsParams,
IsServerSideGroupOpenByDefaultParams,
ModuleRegistry,
ServerSideTransaction,
ServerSideTransactionResult,
createGrid,
enableDevValidations,
} from "ag-grid-community";
import {
RowGroupingModule,
ServerSideRowModelApiModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import {
changePortfolioOnServer,
createRowOnServer,
data,
deletePortfolioOnServer,
} from "./data";
import { FakeServer } from "./fakeServer";
// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
HighlightChangesModule,
RowGroupingModule,
ServerSideRowModelModule,
ServerSideRowModelApiModule,
]);
const columnDefs: ColDef[] = [
{ field: "tradeId" },
{ field: "portfolio", hide: true, rowGroup: true },
{ field: "book" },
{ field: "previous" },
{ field: "current" },
];
let gridApi: GridApi;
const gridOptions: GridOptions = {
columnDefs,
defaultColDef: {
flex: 1,
minWidth: 100,
enableCellChangeFlash: true,
},
autoGroupColumnDef: {
minWidth: 220,
},
isServerSideGroupOpenByDefault: (
params: IsServerSideGroupOpenByDefaultParams,
) => {
return (
params.rowNode.key === "Aggressive" || params.rowNode.key === "Hybrid"
);
},
getRowId: (params: GetRowIdParams) => {
if (params.level === 0) {
return params.data.portfolio;
}
return String(params.data.tradeId);
},
onGridReady: (params: GridReadyEvent) => {
// setup the fake server
const server = new FakeServer(data);
// create datasource with a reference to the fake server
const datasource = getServerSideDatasource(server);
// register the datasource with the grid
params.api.setGridOption("serverSideDatasource", datasource);
},
rowModelType: "serverSide",
};
function getServerSideDatasource(server: any) {
return {
getRows: (params: IServerSideGetRowsParams) => {
const response = server.getData(params.request);
// adding delay to simulate real server call
setTimeout(() => {
if (response.success) {
// call the success callback
params.success({
rowData: response.rows,
rowCount: response.lastRow,
});
} else {
// inform the grid request failed
params.fail();
}
}, 300);
},
};
}
function logResults(
transaction: ServerSideTransaction,
result?: ServerSideTransactionResult,
) {
console.log(
"[Example] - Applied transaction:",
transaction,
"Result:",
result,
);
}
function deleteAllHybrid() {
// NOTE: real applications would be better served listening to a stream of changes from the server instead
const serverResponse: any = deletePortfolioOnServer("Hybrid");
if (!serverResponse.success) {
console.warn("Nothing has changed on the server");
return;
}
if (serverResponse) {
// apply tranaction to keep grid in sync
const transaction = {
remove: [{ portfolio: "Hybrid" }],
};
const result = gridApi!.applyServerSideTransaction(transaction);
logResults(transaction, result);
}
}
function createOneAggressive() {
// NOTE: real applications would be better served listening to a stream of changes from the server instead
const serverResponse: any = createRowOnServer(
"Aggressive",
"Aluminium",
"GL-1",
);
if (!serverResponse.success) {
console.warn("Nothing has changed on the server");
return;
}
if (serverResponse.newGroupCreated) {
// if a new group had to be created, reflect in the grid
const transaction = {
route: [],
add: [{ portfolio: "Aggressive" }],
};
const result = gridApi!.applyServerSideTransaction(transaction);
logResults(transaction, result);
} else {
// if the group already existed, add rows to it
const transaction = {
route: ["Aggressive"],
add: [serverResponse.newRecord],
};
const result = gridApi!.applyServerSideTransaction(transaction);
logResults(transaction, result);
}
}
function updateAggressiveToHybrid() {
// NOTE: real applications would be better served listening to a stream of changes from the server instead
const serverResponse: any = changePortfolioOnServer("Aggressive", "Hybrid");
if (!serverResponse.success) {
console.warn("Nothing has changed on the server");
return;
}
const transaction = {
remove: [{ portfolio: "Aggressive" }],
};
// aggressive group no longer exists, so delete the group
const result = gridApi!.applyServerSideTransaction(transaction);
logResults(transaction, result);
if (serverResponse.newGroupCreated) {
// hybrid group didn't exist, so just create the new group
const t = {
route: [],
add: [{ portfolio: "Hybrid" }],
};
const r = gridApi!.applyServerSideTransaction(t);
logResults(t, r);
} else {
// hybrid group already existed, add rows to it
const t = {
route: ["Hybrid"],
add: serverResponse.updatedRecords,
};
const r = gridApi!.applyServerSideTransaction(t);
logResults(t, r);
}
}
const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).deleteAllHybrid = deleteAllHybrid;
(<any>window).createOneAggressive = createOneAggressive;
(<any>window).updateAggressiveToHybrid = updateAggressiveToHybrid;
}
.example-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
#myGrid {
flex: 1 1 0px;
width: 100%;
}
export let data = [
{
product: 'Palm Oil',
portfolio: 'Aggressive',
book: 'GL-62472',
tradeId: 0,
current: 23558,
previous: 27014,
},
{
product: 'Palm Oil',
portfolio: 'Aggressive',
book: 'GL-62472',
tradeId: 1,
current: 92080,
previous: 97460,
},
{
product: 'Palm Oil',
portfolio: 'Hybrid',
book: 'GL-62473',
tradeId: 2,
current: 1352,
previous: 5835,
},
{
product: 'Palm Oil',
portfolio: 'Hybrid',
book: 'GL-62473',
tradeId: 3,
current: 87685,
previous: 91535,
},
{
product: 'Palm Oil',
portfolio: 'Defensive',
book: 'GL-62474',
tradeId: 4,
current: 25263,
previous: 26374,
},
{
product: 'Palm Oil',
portfolio: 'Defensive',
book: 'GL-62474',
tradeId: 5,
current: 65201,
previous: 69745,
},
{
product: 'Palm Oil',
portfolio: 'Income',
book: 'GL-62475',
tradeId: 6,
current: 48405,
previous: 50367,
},
{
product: 'Palm Oil',
portfolio: 'Income',
book: 'GL-62475',
tradeId: 7,
current: 65361,
previous: 64564,
},
{
product: 'Palm Oil',
portfolio: 'Speculative',
book: 'GL-62476',
tradeId: 8,
current: 94747,
previous: 94067,
},
{
product: 'Palm Oil',
portfolio: 'Speculative',
book: 'GL-62476',
tradeId: 9,
current: 28967,
previous: 32447,
},
];
export function deletePortfolioOnServer(portfolio) {
const oldDataSize = data.length;
const filteredData = data.filter((record) => record.portfolio !== portfolio);
// need to maintain original data reference
data.length = 0;
data.push(...filteredData);
return {
success: oldDataSize !== data.length,
};
}
let currentServerRecordId = data.length;
export function createRowOnServer(portfolio, product, book) {
const groupDidExist = data.some((record) => record.portfolio === 'Aggressive');
const newRecord = {
tradeId: ++currentServerRecordId,
portfolio: portfolio,
product: product,
book: book,
current: 0,
previous: 0,
};
data.push(newRecord);
return {
success: true,
newGroupCreated: !groupDidExist,
newRecord: newRecord,
};
}
export function changePortfolioOnServer(oldPortfolio, newPortfolio) {
const groupDidExist = data.some((record) => record.portfolio === newPortfolio);
const updatedRecords = [];
data.forEach((record) => {
if (record.portfolio === oldPortfolio) {
record.portfolio = newPortfolio;
updatedRecords.push(record);
}
});
return {
success: !!updatedRecords.length,
newGroupCreated: !groupDidExist,
updatedRecords: updatedRecords,
};
}
export function FakeServer(data) {
alasql.options.cache = false;
return {
getData: function (request) {
const results = executeQuery(request);
return {
success: true,
rows: results,
};
},
};
function executeQuery(request) {
const groupByResult = executeRowGroupQuery(request);
return groupByResult;
}
function executeRowGroupQuery(request) {
const groupByQuery = buildGroupBySql(request);
console.log('[FakeServer] - about to execute row group query:', groupByQuery);
return alasql(groupByQuery, [data]);
}
function buildGroupBySql(request) {
return (
selectSql(request) +
' FROM ?' +
whereSql(request) +
groupBySql(request) +
orderBySql(request) +
limitSql(request)
);
}
function selectSql(request) {
const rowGroupCols = request.rowGroupCols;
const valueCols = request.valueCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const colsToSelect = [rowGroupCol.id];
valueCols.forEach(function (valueCol) {
colsToSelect.push(valueCol.aggFunc + '(' + valueCol.id + ') AS ' + valueCol.id);
});
return 'SELECT ' + colsToSelect.join(', ');
}
return 'SELECT *';
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
function groupBySql(request) {
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
return ' GROUP BY ' + rowGroupCol.id + ' HAVING count(*) > 0';
}
return '';
}
function orderBySql(request) {
const sortModel = request.sortModel;
if (sortModel.length === 0) return '';
const sorts = sortModel.map(function (s) {
return s.colId + ' ' + s.sort.toUpperCase();
});
return ' ORDER BY ' + sorts.join(', ');
}
function limitSql(request) {
if (request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level
return rowGroupCols.length > groupKeys.length;
}
function getLastRowIndex(request, results) {
if (!results || results.length === 0) {
return null;
}
if (request.endRow == undefined || request.startRow == undefined) {
return results.length;
}
const currentLastRow = request.startRow + results.length;
return currentLastRow <= request.endRow ? currentLastRow : -1;
}
}
// IE Workaround - as templates literals are not supported
function interpolate(str, o) {
return str.replace(/{([^{}]*)}/g, function (a, b) {
const r = o[b];
return typeof r === 'string' || typeof r === 'number' ? r : a;
});
}
<div class="example-wrapper">
<div style="margin-bottom: 5px">
<button onClick="createOneAggressive()">Add new 'Aggressive'</button>
<button onClick="updateAggressiveToHybrid()">Move all 'Aggressive' to 'Hybrid'</button>
<button onClick="deleteAllHybrid()">Remove all 'Hybrid'</button>
</div>
<div id="myGrid"></div>
</div>
Asynchronous Updates Copy Link
When processing many updates rapidly, the grid will perform more smoothly if the changes are batched (as this can prevent excessive rendering). The grid can batch these changes for you without negatively impacting the user experience, and in most cases improving it.
Batch apply transactions to the server side row model. |
When using asynchronous transactions, the grid delays any transactions received within a time window (specified using asyncTransactionWaitMillis) and executes them together when the window has passed.
The snippet below demonstrates three asynchronous transactions applied sequentially, however because these transactions are asynchronously batched, the grid would only update the DOM once.
// due to asynchronous batching, the following transactions are applied together preventing unnecessary DOM updates
api.applyServerSideTransactionAsync({
add: [{ tradeId: 101, portfolio: 'Aggressive', product: 'Aluminium', book: 'GL-62472', current: 57969 }],
});
api.applyServerSideTransactionAsync({
update: [{ tradeId: 102, portfolio: 'Aggressive', product: 'Aluminium', book: 'GL-624723', current: 58927 }],
});
api.applyServerSideTransactionAsync({
remove: [{ tradeId: 103 }],
});In the example below, note the following:
- After starting the updates, 1 row is created, 10 rows are updated, and 1 row is deleted every 10 milliseconds.
- The transactions are batched, and only executed once every second.
import {
ColDef,
GetRowIdParams,
GridApi,
GridOptions,
GridReadyEvent,
HighlightChangesModule,
IServerSideGetRowsParams,
ModuleRegistry,
ServerSideTransaction,
createGrid,
enableDevValidations,
} from "ag-grid-community";
import {
ServerSideRowModelApiModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { data, dataObservers, randomUpdates } from "./data";
import { FakeServer } from "./fakeServer";
// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
HighlightChangesModule,
ServerSideRowModelModule,
ServerSideRowModelApiModule,
]);
const columnDefs: ColDef[] = [
{ field: "tradeId" },
{ field: "portfolio" },
{ field: "book" },
{ field: "previous" },
{ field: "current" },
{
field: "lastUpdated",
wrapHeaderText: true,
autoHeaderHeight: true,
valueFormatter: (params) => {
const ts = params.data!.lastUpdated;
if (ts) {
const hh_mm_ss = ts.toLocaleString().split(" ")[1];
const SSS = ts.getMilliseconds();
return `${hh_mm_ss}:${SSS}`;
}
return "";
},
},
{ field: "updateCount" },
];
let gridApi: GridApi;
const gridOptions: GridOptions = {
columnDefs,
defaultColDef: {
flex: 1,
minWidth: 100,
enableCellChangeFlash: true,
},
autoGroupColumnDef: {
minWidth: 220,
},
getRowId: (params: GetRowIdParams) => {
let rowId = "";
if (params.parentKeys && params.parentKeys.length) {
rowId += params.parentKeys.join("-") + "-";
}
if (params.data.tradeId != null) {
rowId += params.data.tradeId;
}
return rowId;
},
onGridReady: (params: GridReadyEvent) => {
disable("#stopUpdates", true);
// setup the fake server
const server = FakeServer(data);
// create datasource with a reference to the fake server
const datasource = getServerSideDatasource(server);
// register the datasource with the grid
params.api.setGridOption("serverSideDatasource", datasource);
// register interest in data changes
dataObservers.push((t: ServerSideTransaction) => {
params.api.applyServerSideTransactionAsync(t);
});
},
asyncTransactionWaitMillis: 1000,
rowModelType: "serverSide",
};
function getServerSideDatasource(server: any) {
return {
getRows: (params: IServerSideGetRowsParams) => {
const response = server.getData(params.request);
// adding delay to simulate real server call
setTimeout(() => {
if (response.success) {
// call the success callback
params.success({
rowData: response.rows,
rowCount: response.lastRow,
});
} else {
// inform the grid request failed
params.fail();
}
}, 300);
},
};
}
let interval: any;
function startUpdates() {
interval = setInterval(
() => randomUpdates({ numUpdate: 10, numAdd: 1, numRemove: 1 }),
10,
);
disable("#stopUpdates", false);
disable("#startUpdates", true);
}
function stopUpdates() {
if (interval !== undefined) {
clearInterval(interval);
}
disable("#stopUpdates", true);
disable("#startUpdates", false);
}
function disable(id: string, disabled: boolean) {
document.querySelector<HTMLInputElement>(id)!.disabled = disabled;
}
const gridDiv = document.querySelector<HTMLElement>("#myGrid")!;
gridApi = createGrid(gridDiv, gridOptions);
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).startUpdates = startUpdates;
(<any>window).stopUpdates = stopUpdates;
}
.example-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
#myGrid {
flex: 1 1 0px;
width: 100%;
}
const MIN_BOOK_COUNT = 1;
const MAX_BOOK_COUNT = 5;
const MIN_TRADE_COUNT = 1;
const MAX_TRADE_COUNT = 5;
const products = [
'Palm Oil',
'Rubber',
'Wool',
'Amber',
'Copper',
'Lead',
'Zinc',
'Tin',
'Aluminium',
'Aluminium Alloy',
'Nickel',
'Cobalt',
'Molybdenum',
'Recycled Steel',
'Corn',
'Oats',
'Rough Rice',
'Soybeans',
'Rapeseed',
'Soybean Meal',
'Soybean Oil',
'Wheat',
'Milk',
'Coca',
'Coffee C',
'Cotton No.2',
'Sugar No.11',
'Sugar No.14',
];
const portfolios = ['Aggressive', 'Defensive', 'Income', 'Speculative', 'Hybrid'];
let nextTradeId = 0;
let nextBookId = 62472;
export var data = [];
// IIFE to create initial data
(function () {
const lastUpdated = new Date();
for (let i = 0; i < products.length; i++) {
const product = products[i];
for (let j = 0; j < portfolios.length; j++) {
const portfolio = portfolios[j];
const bookCount = randomBetween(MAX_BOOK_COUNT, MIN_BOOK_COUNT);
for (let k = 0; k < bookCount; k++) {
const book = createBookName();
const tradeCount = randomBetween(MAX_TRADE_COUNT, MIN_TRADE_COUNT);
for (let l = 0; l < tradeCount; l++) {
const trade = createTradeRecord(product, portfolio, book);
trade.updateCount = 0;
trade.lastUpdated = lastUpdated;
data.push(trade);
}
}
}
}
})();
export var dataObservers = [];
export function randomUpdates({ numRemove, numAdd, numUpdate }) {
// removes
const remove = [];
for (let i = 0; i < Math.ceil(numRemove); i++) {
const idx = randomBetween(0, data.length - 1);
const d = data[idx];
data.splice(idx, 1);
remove.push(d);
}
// updates
const update = [];
for (let i = 0; i < numUpdate; i++) {
const idx = randomBetween(0, data.length - 1);
const d = data[idx];
d.previous = d.current;
d.current = d.previous + 13;
d.lastUpdated = new Date();
d.updateCount = ++d.updateCount;
update.push(d);
}
// adds
const add = [];
const lastUpdate = new Date();
for (let i = 0; i < Math.ceil(numAdd); i++) {
const product = products[randomBetween(0, products.length - 1)];
const portfolio = portfolios[randomBetween(0, portfolios.length - 1)];
const book = createBookName();
const newRecord = createTradeRecord(product, portfolio, book);
newRecord.lastUpdated = lastUpdate;
newRecord.updateCount = 0;
add.push(newRecord);
}
data.push(...add);
// notify observers
dataObservers.forEach((obs) => obs({ update, add, remove }));
}
function randomBetween(min, max) {
return Math.floor(window.agRandom() * (max - min + 1)) + min;
}
function createTradeRecord(product, portfolio, book) {
const current = Math.floor(window.agRandom() * 100000) + 100;
const previous = current + Math.floor(window.agRandom() * 10000) - 2000;
const trade = {
product: product,
portfolio: portfolio,
book: book,
tradeId: createTradeId(),
submitterID: randomBetween(10, 1000),
submitterDealID: randomBetween(10, 1000),
dealType: window.agRandom() < 0.2 ? 'Physical' : 'Financial',
bidFlag: window.agRandom() < 0.5 ? 'Buy' : 'Sell',
current: current,
previous: previous,
pl1: randomBetween(100, 1000),
pl2: randomBetween(100, 1000),
gainDx: randomBetween(100, 1000),
sxPx: randomBetween(100, 1000),
_99Out: randomBetween(100, 1000),
};
return trade;
}
function createBookName() {
return 'GL-' + nextBookId++;
}
function createTradeId() {
return nextTradeId++;
}
export function FakeServer(data) {
alasql.options.cache = false;
return {
getData: function (request) {
const results = executeQuery(request);
const resultSize = executeQuery({ ...request, endRow: undefined }, true).length;
return {
success: true,
rows: results,
lastRow: resultSize,
};
},
};
function executeQuery(request, suppressLogging) {
const groupByResult = executeRowGroupQuery(request, suppressLogging);
return groupByResult;
}
function executeRowGroupQuery(request, suppressLogging) {
const groupByQuery = buildGroupBySql(request);
if (!suppressLogging) {
console.log('[FakeServer] - about to execute row group query:', groupByQuery);
}
return alasql(groupByQuery, [data]);
}
function buildGroupBySql(request) {
return (
selectSql(request) +
' FROM ?' +
whereSql(request) +
groupBySql(request) +
orderBySql(request) +
limitSql(request)
);
}
function selectSql(request) {
const rowGroupCols = request.rowGroupCols;
const valueCols = request.valueCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const colsToSelect = [rowGroupCol.id];
valueCols.forEach(function (valueCol) {
colsToSelect.push(valueCol.aggFunc + '(' + valueCol.id + ') AS ' + valueCol.id);
});
return 'SELECT ' + colsToSelect.join(', ');
}
return 'SELECT *';
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
function groupBySql(request) {
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
return ' GROUP BY ' + rowGroupCol.id + ' HAVING count(*) > 0';
}
return '';
}
function orderBySql(request) {
const sortModel = request.sortModel;
if (sortModel.length === 0) return '';
const sorts = sortModel.map(function (s) {
return s.colId + ' ' + s.sort.toUpperCase();
});
return ' ORDER BY ' + sorts.join(', ');
}
function limitSql(request) {
if (request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level
return rowGroupCols.length > groupKeys.length;
}
function getLastRowIndex(request, results) {
if (!results || results.length === 0) {
return null;
}
if (request.endRow == undefined || request.startRow == undefined) {
return results.length;
}
const currentLastRow = request.startRow + results.length;
return currentLastRow <= request.endRow ? currentLastRow : -1;
}
}
// IE Workaround - as templates literals are not supported
function interpolate(str, o) {
return str.replace(/{([^{}]*)}/g, function (a, b) {
const r = o[b];
return typeof r === 'string' || typeof r === 'number' ? r : a;
});
}
<div class="example-wrapper">
<div style="margin-bottom: 5px">
<button id="startUpdates" onclick="startUpdates()">Start Updates</button>
<button id="stopUpdates" onclick="stopUpdates()">Stop Updates</button>
</div>
<div id="myGrid"></div>
</div>
Showcase Example Copy Link
The following demonstrates a more complex example of transactions, it shows subscribing to a source of updates to provide the changes, while using dynamic row grouping, aggregation, and child counts. All of which react to the changes caused by the transactions.
In the example below, note the following:
- After starting the updates, 2 rows are created, 5 rows are updated, and 2 rows are deleted once every second.
- Groups are created or destroyed when necessary by using transactions.
- The group panel has been enabled, allowing a dynamic configuration of groups.
- The group child counts and aggregations update in sync with changes to the leaf rows.
import {
ColDef,
ColumnApiModule,
ColumnRowGroupChangedEvent,
GetRowIdParams,
GridApi,
GridOptions,
HighlightChangesModule,
IServerSideGetRowsParams,
IsServerSideGroupOpenByDefaultParams,
ModuleRegistry,
ServerSideTransaction,
TextFilterModule,
createGrid,
enableDevValidations,
} from "ag-grid-community";
import {
RowGroupingModule,
RowGroupingPanelModule,
ServerSideRowModelApiModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { getFakeServer, registerObserver } from "./fakeServer";
// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
TextFilterModule,
HighlightChangesModule,
ColumnApiModule,
RowGroupingModule,
ServerSideRowModelModule,
ServerSideRowModelApiModule,
RowGroupingPanelModule,
]);
const columnDefs: ColDef[] = [
{ field: "tradeId" },
{
field: "product",
rowGroup: true,
enableRowGroup: true,
hide: true,
},
{
field: "portfolio",
rowGroup: true,
enableRowGroup: true,
hide: true,
},
{
field: "book",
rowGroup: true,
enableRowGroup: true,
hide: true,
},
{ field: "previous", aggFunc: "sum" },
{ field: "current", aggFunc: "sum" },
];
let gridApi: GridApi;
const gridOptions: GridOptions = {
columnDefs,
defaultColDef: {
flex: 1,
minWidth: 100,
enableCellChangeFlash: true,
},
autoGroupColumnDef: {
minWidth: 220,
},
rowGroupPanelShow: "always",
purgeClosedRowNodes: true,
rowModelType: "serverSide",
getRowId: getRowId,
isServerSideGroupOpenByDefault: isServerSideGroupOpenByDefault,
onColumnRowGroupChanged: onColumnRowGroupChanged,
// fetch group child count from 'childCount' returned by the server
getChildCount: getChildCount,
onGridReady: (params) => {
disable("#stopUpdates", true);
// create datasource with a reference to the fake server
const datasource = getServerSideDatasource(getFakeServer());
// register the datasource with the grid
params.api.setGridOption("serverSideDatasource", datasource);
// register interest in data changes
registerObserver({
transactionFunc: (t: ServerSideTransaction) =>
params.api.applyServerSideTransactionAsync(t),
groupedFields: ["product", "portfolio", "book"],
});
},
};
function startUpdates() {
getFakeServer().randomUpdates();
disable("#startUpdates", true);
disable("#stopUpdates", false);
}
function stopUpdates() {
getFakeServer().stopUpdates();
disable("#stopUpdates", true);
disable("#startUpdates", false);
}
function getChildCount(data: any) {
return data ? data.childCount : undefined;
}
function disable(id: string, disabled: boolean) {
document.querySelector<HTMLInputElement>(id)!.disabled = disabled;
}
function getServerSideDatasource(server: any) {
return {
getRows: (params: IServerSideGetRowsParams) => {
console.log("[Datasource] - rows requested by grid: ", params.request);
const response = server.getData(params.request);
// adding delay to simulate real server call
setTimeout(() => {
if (response.success) {
// call the success callback
params.success({
rowData: response.rows,
rowCount: response.lastRow,
});
} else {
// inform the grid request failed
params.fail();
}
}, 300);
},
};
}
function getRowId(params: GetRowIdParams) {
let rowId = "";
if (params.parentKeys && params.parentKeys.length) {
rowId += params.parentKeys.join("-") + "-";
}
const groupCols = params.api.getRowGroupColumns();
if (groupCols.length > params.level) {
const thisGroupCol = groupCols[params.level];
rowId += params.data[thisGroupCol.getColDef().field!] + "-";
}
if (params.data.tradeId != null) {
rowId += params.data.tradeId;
}
return rowId;
}
function onColumnRowGroupChanged(event: ColumnRowGroupChangedEvent) {
const colState = event.api.getColumnState();
const groupedColumns = colState.filter((state) => state.rowGroup);
groupedColumns.sort((a, b) => a.rowGroupIndex! - b.rowGroupIndex!);
const groupedFields = groupedColumns.map((col) => col.colId);
registerObserver({
transactionFunc: (t: ServerSideTransaction) =>
gridApi!.applyServerSideTransactionAsync(t),
groupedFields: groupedFields.length === 0 ? undefined : groupedFields,
});
}
function isServerSideGroupOpenByDefault(
params: IsServerSideGroupOpenByDefaultParams,
) {
const route = params.rowNode.getRoute();
if (!route) {
return false;
}
const routeAsString = route.join(",");
return (
["Wool", "Wool,Aggressive", "Wool,Aggressive,GL-62502"].indexOf(
routeAsString,
) >= 0
);
}
const gridDiv = document.querySelector<HTMLElement>("#myGrid");
gridApi = createGrid(gridDiv!, gridOptions);
if (typeof window !== "undefined") {
// Attach external event handlers to window so they can be called from index.html
(<any>window).startUpdates = startUpdates;
(<any>window).stopUpdates = stopUpdates;
}
.grid-container {
display: flex;
flex-direction: column;
height: 100%;
row-gap: 5px;
}
#myGrid {
flex: 1 1 0px;
}
function FakeServer() {
alasql.options.cache = false;
let intervals = [];
return {
randomUpdates: () => {
intervals.push(setInterval(() => randomTransaction({ numUpdate: 5, numAdd: 2, numRemove: 2 }), 1000));
},
stopUpdates: () => {
intervals.forEach(clearInterval);
intervals = [];
},
getData: function (request) {
const results = executeQuery(request);
return {
success: true,
rows: results,
};
},
getAggValues: function (groupRow) {
const whereClause = Object.entries(groupRow)
.map(([field, val]) => `${field} = "${val}"`)
.join(' AND ');
const SQL = `
SELECT SUM(current) as current, SUM(previous) as previous, COUNT(tradeId) as childCount FROM ? WHERE ${whereClause}
`;
return alasql(SQL, [data])[0];
},
};
function executeQuery(request) {
const groupByResult = executeRowGroupQuery(request);
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (!isDoingGrouping(rowGroupCols, groupKeys)) {
return groupByResult;
}
const groupsToUse = request.rowGroupCols.slice(groupKeys.length, groupKeys.length + 1);
const groupColId = groupsToUse[0].id;
const childCountResult = executeGroupChildCountsQuery(request, groupColId);
// add 'childCount' to group results
return groupByResult.map(function (group) {
group['childCount'] = childCountResult[group[groupColId]];
return group;
});
}
function executeRowGroupQuery(request) {
const groupByQuery = buildGroupBySql(request);
return alasql(groupByQuery, [data]);
}
function executeGroupChildCountsQuery(request, groupId) {
const SQL = interpolate('SELECT {0} FROM ? pivot (count({0}) for {0})' + whereSql(request), [groupId]);
return alasql(SQL, [data])[0];
}
function buildGroupBySql(request) {
return (
selectSql(request) +
' FROM ?' +
whereSql(request) +
groupBySql(request) +
orderBySql(request) +
limitSql(request)
);
}
function selectSql(request) {
const rowGroupCols = request.rowGroupCols;
const valueCols = request.valueCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const colsToSelect = [rowGroupCol.id];
valueCols.forEach(function (valueCol) {
colsToSelect.push(valueCol.aggFunc + '(' + valueCol.id + ') AS ' + valueCol.id);
});
return 'SELECT ' + colsToSelect.join(', ');
}
return 'SELECT *';
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
function groupBySql(request) {
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
return ' GROUP BY ' + rowGroupCol.id + ' HAVING count(*) > 0';
}
return '';
}
function orderBySql(request) {
const sortModel = request.sortModel;
if (sortModel.length === 0) return '';
const sorts = sortModel.map(function (s) {
return s.colId + ' ' + s.sort.toUpperCase();
});
return ' ORDER BY ' + sorts.join(', ');
}
function limitSql(request) {
if (request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level
return rowGroupCols.length > groupKeys.length;
}
function getLastRowIndex(request, results) {
if (!results || results.length === 0) {
return null;
}
if (request.endRow == undefined || request.startRow == undefined) {
return results.length;
}
const currentLastRow = request.startRow + results.length;
return currentLastRow <= request.endRow ? currentLastRow : -1;
}
}
let fakeServerInstance: FakeServer;
export function getFakeServer() {
if (!fakeServerInstance) {
fakeServerInstance = new FakeServer();
}
return fakeServerInstance;
}
// IE Workaround - as templates literals are not supported
function interpolate(str, o) {
return str.replace(/{([^{}]*)}/g, function (a, b) {
const r = o[b];
return typeof r === 'string' || typeof r === 'number' ? r : a;
});
}
const BOOK_COUNT = 3;
const MIN_TRADE_COUNT = 1;
const MAX_TRADE_COUNT = 10;
const products = [
'Palm Oil',
'Rubber',
'Wool',
'Amber',
'Copper',
'Lead',
'Zinc',
'Tin',
'Aluminium',
'Aluminium Alloy',
'Nickel',
'Cobalt',
'Molybdenum',
'Recycled Steel',
'Corn',
'Oats',
];
const portfolios = ['Aggressive', 'Defensive', 'Income', 'Speculative', 'Hybrid'];
let nextTradeId = 0;
const FIRST_BOOK_ID = 62472;
const PRODUCT_BOOK_START = {};
products.forEach((product, idx) => {
PRODUCT_BOOK_START[product] = FIRST_BOOK_ID + portfolios.length * BOOK_COUNT * idx;
});
const PORTFOLIO_BOOK_OFFSET = {};
portfolios.forEach((portfolio, idx) => {
PORTFOLIO_BOOK_OFFSET[portfolio] = idx * BOOK_COUNT;
});
let nextBookId = 62472;
export const data = [];
// IIFE to create initial data
(function () {
const lastUpdated = new Date();
for (let i = 0; i < products.length; i++) {
const product = products[i];
for (let j = 0; j < portfolios.length; j++) {
const portfolio = portfolios[j];
for (let k = 0; k < BOOK_COUNT; k++) {
const book = createBookName();
const tradeCount = randomBetween(MAX_TRADE_COUNT, MIN_TRADE_COUNT);
for (let l = 0; l < tradeCount; l++) {
const trade = createTradeRecord(product, portfolio, book);
trade.updateCount = 0;
trade.lastUpdated = lastUpdated;
data.push(trade);
}
}
}
}
})();
export const dataObservers = [];
export const registerObserver = ({ transactionFunc, groupedFields }) => {
const existingObserver = dataObservers.find(({ transactionFunc: oldFunc }) => oldFunc === transactionFunc);
if (existingObserver) {
existingObserver.groupedFields = groupedFields;
return;
}
dataObservers.push({
transactionFunc,
groupedFields,
});
};
const uniqueQueries = new Map();
export function randomTransaction({ numAdd, numUpdate, numRemove }) {
uniqueQueries.clear();
// updates
const update = [];
for (let i = 0; i < numUpdate && data.length; i++) {
const idx = randomBetween(0, data.length - 1);
const d = data[idx];
d.previous = d.current;
d.current = d.previous + 13;
d.lastUpdated = new Date();
d.updateCount = ++d.updateCount;
update.push(d);
}
// adds
const add = [];
const lastUpdate = new Date();
for (let i = 0; i < numAdd; i++) {
const product = products[randomBetween(0, products.length - 1)];
const portfolio = portfolios[randomBetween(0, portfolios.length - 1)];
const bookStart = PRODUCT_BOOK_START[product] + PORTFOLIO_BOOK_OFFSET[portfolio];
const book = 'GL-' + randomBetween(bookStart, bookStart + BOOK_COUNT - 1);
const newRecord = createTradeRecord(product, portfolio, book);
newRecord.lastUpdated = lastUpdate;
newRecord.updateCount = 0;
add.push(newRecord);
}
// insert new rows at the end
data.push(...add);
// removes
const remove = [];
for (let i = 0; i < numRemove && data.length; i++) {
const idx = randomBetween(0, data.length - 1);
const d = data[idx];
data.splice(idx, 1);
remove.push(d);
}
dataObservers.forEach(({ transactionFunc, groupedFields }) => {
const routedTransactions = {};
translateRowsToRoutes({
rows: update,
op: 'update',
fields: groupedFields,
mutableTransactionObj: routedTransactions,
});
translateRowsToRoutes({
rows: remove,
op: 'remove',
fields: groupedFields,
mutableTransactionObj: routedTransactions,
});
translateRowsToRoutes({
rows: add,
op: 'add',
fields: groupedFields,
mutableTransactionObj: routedTransactions,
});
// may want to filter duplicates here
Object.values(routedTransactions).forEach(transactionFunc);
});
}
const translateRowsToRoutes = ({ rows, op, fields, mutableTransactionObj }) => {
rows.forEach((item) => {
for (let i = 0; i < fields.length; i++) {
const route = fields.slice(0, i).map((field) => item[field]);
const routeId = route.join('-');
const groupRowFields = fields.slice(0, i + 1);
const groupRow = Object.fromEntries(groupRowFields.map((field) => [field, item[field]]));
// does a row belonging to this group already exist
const doesGroupExist = data.some(
(row) => row !== item && groupRowFields.every((field) => groupRow[field] === row[field])
);
const stringifiedRow = JSON.stringify(groupRow);
let aggValues;
if (uniqueQueries.has(stringifiedRow)) {
aggValues = uniqueQueries.get(stringifiedRow);
} else {
aggValues = getFakeServer().getAggValues(groupRow);
uniqueQueries.set(stringifiedRow, aggValues);
}
const newGroupItem = { ...groupRow, ...aggValues };
// if not, create a new group row instead
if (!doesGroupExist) {
const existingTransaction = mutableTransactionObj[routeId] || {};
mutableTransactionObj[routeId] = {
...existingTransaction,
route,
[op]: [...(existingTransaction[op] ?? []), newGroupItem],
};
return;
}
// if group does exist, update aggregations
const existingTransaction = mutableTransactionObj[routeId] || {};
mutableTransactionObj[routeId] = {
...existingTransaction,
route: route,
update: [...(existingTransaction.update ?? []), newGroupItem],
};
}
// no groups need created, create the leaf row
const route = fields.map((field) => item[field]);
const routeId = route.join('-');
const existingTransaction = mutableTransactionObj[routeId] || {};
mutableTransactionObj[routeId] = {
route,
[op]: [...(existingTransaction[op] ?? []), item],
};
});
};
function randomBetween(min, max) {
return Math.floor(window.agRandom() * (max - min + 1)) + min;
}
function createTradeRecord(product, portfolio, book) {
const current = Math.floor(window.agRandom() * 100000) + 100;
const previous = current + Math.floor(window.agRandom() * 10000) - 2000;
const trade = {
product: product,
portfolio: portfolio,
book: book,
tradeId: createTradeId(),
submitterID: randomBetween(10, 1000),
submitterDealID: randomBetween(10, 1000),
dealType: window.agRandom() < 0.2 ? 'Physical' : 'Financial',
bidFlag: window.agRandom() < 0.5 ? 'Buy' : 'Sell',
current: current,
previous: previous,
pl1: randomBetween(100, 1000),
pl2: randomBetween(100, 1000),
gainDx: randomBetween(100, 1000),
sxPx: randomBetween(100, 1000),
_99Out: randomBetween(100, 1000),
};
return trade;
}
function createBookName() {
return 'GL-' + nextBookId++;
}
function createTradeId() {
return nextTradeId++;
}
<div class="grid-container">
<div>
<button id="startUpdates" onclick="startUpdates()">Start Updates</button>
<button id="stopUpdates" onclick="stopUpdates()">Stop Updates</button>
</div>
<div id="myGrid"></div>
</div>
Tree Data Copy Link
Transactions are also supported when using tree data. See this documented on the SSRM Tree Data page.