| name | servoy-grids |
| description | Use when working on Servoy ag-grid table components: powergrid, datagrid, datasettable, groupingtable, aggrid, ag-grid, Power Grid, Data Grid, renderData, appendLazyRequestData, onColumnDataChange, myFoundset, foundset grid, JSDataset, rowGroupIndex, gridOptions, columnDef. Covers bug fixing and feature implementation for all four component variants (Angular TS and AngularJS). |
Servoy AG-Grid Components
This repository contains four Servoy table view components built on top of ag-Grid:
| Component | Display name | Framework | Data source |
|---|
powergrid | Power Grid | Angular TypeScript | JSDataset |
datagrid | Data Grid | Angular TypeScript | Servoy foundset |
datasettable | Power Grid (legacy) | AngularJS JavaScript | JSDataset |
groupingtable | Data Grid (legacy) | AngularJS JavaScript | Servoy foundset |
The Angular components (powergrid, datagrid) are drop-in replacements for their AngularJS counterparts and reuse the same .spec and _doc.js files.
1. Component Map — Which Files to Read/Edit
powergrid (Angular, dataset-based)
- Spec:
datasettable/datasettable.spec
- Doc:
datasettable/datasettable_doc.js
- Main impl:
projects/nggrids/src/powergrid/powergrid.ts
- Service:
projects/nggrids/src/powergrid/powergrid.service.ts
- Template:
projects/nggrids/src/powergrid/powergrid.html
- Base class:
projects/nggrids/src/nggrid.ts
- Build required: yes —
npm run build in /aggrid/
datagrid (Angular, foundset-based)
- Spec:
groupingtable/groupingtable.spec
- Doc:
groupingtable/groupingtable_doc.js
- Main impl:
projects/nggrids/src/datagrid/datagrid.ts
- Service:
projects/nggrids/src/datagrid/datagrid.service.ts
- Template:
projects/nggrids/src/datagrid/datagrid.html
- Base class:
projects/nggrids/src/nggrid.ts
- Build required: yes —
npm run build in /aggrid/
datasettable (AngularJS, dataset-based)
- Spec:
datasettable/datasettable.spec (shared with powergrid)
- Doc:
datasettable/datasettable_doc.js (shared with powergrid)
- Main impl:
datasettable/datasettable.js
- Server script:
datasettable/datasettable_server.js
- Group cell renderer:
datasettable/datasettablegroupcellrenderer.js
- Build required: no
groupingtable (AngularJS, foundset-based)
- Spec:
groupingtable/groupingtable.spec (shared with datagrid)
- Doc:
groupingtable/groupingtable_doc.js (shared with datagrid)
- Main impl:
groupingtable/groupingtable.js
- Server script:
groupingtable/groupingtable_server.js
- Build required: no
Shared Angular source (both powergrid and datagrid)
- Editors:
projects/nggrids/src/editors/ — editor.ts, texteditor.ts, datepicker.ts, selecteditor.ts, typeaheadeditor.ts, formeditor.ts
- Filters:
projects/nggrids/src/filters/ — filter.ts, datefilter.ts, radiofilter.ts, valuelistfilter.ts
- Commons:
projects/nggrids/src/datagrid/commons/ — registration.service.ts, tooltip.ts
When modifying model properties, handlers, or API methods
Always update three files in sync:
.spec — the Servoy component definition (model/handlers/api/types blocks)
_doc.js — the JSDoc documentation file
- Main
.ts or .js implementation
2. Servoy vs ag-Grid Layering
Both components wrap ag-Grid. The Servoy layer exposes a curated subset of ag-Grid's API. When a behaviour is needed that the Servoy API does not expose directly:
- Grid-level: use the
gridOptions map property — passed through to ag-Grid's GridOptions
- Column-level: use the
columnDef map property per column — passed through to ag-Grid's ColDef
Useful ag-Grid options accessible via gridOptions
| Option | Type | Description |
|---|
groupDisplayType | 'groupRows' | 'multipleColumns' | 'singleColumn' | 'custom' | How group rows are displayed |
groupDefaultExpanded | number | How many levels expanded by default (-1 = all) |
suppressAggFuncInHeader | boolean | Hide agg function name in column header |
pivotPanelShow | 'always' | 'onlyWhenPivoting' | 'never' | Pivot panel visibility |
rowSelection | object | Row selection configuration object |
suppressRowClickSelection | boolean | Don't select rows on click |
getRowId | function | Custom row identity function |
animateRows | boolean | Animate row changes |
suppressAggAtRootLevel | boolean | Suppress aggregation at root level |
treeData | boolean | Enable tree data mode |
Useful ag-Grid options accessible via column.columnDef
| Option | Type | Description |
|---|
pinned | 'left' | 'right' | Pin column to side |
flex | number | Flex-grow ratio for column width |
cellClass | string | string[] | CSS class(es) for cells |
cellStyle | object | Inline CSS for cells |
comparator | function | Custom sort comparator |
cellEditorPopup | boolean | Show editor as popup |
cellEditorPopupPosition | 'over' | 'under' | Popup editor position |
suppressMovable | boolean | Prevent column from being moved |
lockPosition | 'left' | 'right' | Lock column to position |
lockVisible | boolean | Prevent column show/hide via UI |
wrapText | boolean | Wrap cell text |
autoHeight | boolean | Auto-size row height based on cell content |
enableCellChangeFlash | boolean | Flash cell on value change |
colSpan | function | Make cell span multiple columns |
spanRows | boolean | function | Merge cells with equal values |
Important: some ag-Grid options are marked Initial — they are only read at grid creation. For those, setting them via gridOptions after the grid is rendered has no effect; a full re-render is needed.
Reference: https://www.ag-grid.com/javascript-data-grid/grid-options/ | https://www.ag-grid.com/javascript-data-grid/column-properties/
3. Core Data Model Difference
This is the most important distinction between the two component families.
powergrid / datasettable — dataset-based
- Data comes from a
JSDataset and is loaded explicitly by calling renderData(dataset, pks?)
- Row identity is defined by pk string values stored in
rowData object properties
pks must be passed to renderData before updateRows or deleteRows will work
- CRUD operations (
newRows, updateRows, deleteRows) are available on the Angular powergrid only — not in the AngularJS datasettable
- Lazy loading: set
useLazyLoading=true, implement onLazyLoadingGetRows, call appendLazyRequestData(dataset, lastRowIndex) inside it
- When
lastRowIndex equals total rows on server, lazy loading stops; if never set, grid behaves as infinite scroll
datagrid / groupingtable — foundset-based
- Data is bound via the
myFoundset property — a live Servoy foundset
- Servoy manages record identity and lifecycle; no manual data load call
- In grouping mode,
foundsetIndex is always -1 in all cell event handlers — use record object and its pks to identify the row
- Grouping requires a single primary key — multiple PKs are not supported
- Calculations, aggregations, and form variables must not be used in grouped columns — set
enableRowGroup=false on such columns
- Viewport changes are reflected automatically; out-of-viewport changes require action:
notifyDataChange() — non-disruptive; shows orange refresh button to the user
refreshData() — forces full refresh but collapses all group nodes — bad UX, use sparingly
- Use
notifyDataChange() in onDataBroadcast and onAfterRecordInsert/Update/Delete for background data changes
4. Handler Signatures
powergrid / datasettable
onRowSelected(rowData: Object, selected: Boolean, event?: JSEvent)
onCellClick(rowData: Object, columnId?: String, cellData?: Object, event?: JSEvent, dataTarget?: String)
onCellDoubleClick(rowData: Object, columnId?: String, cellData?: Object, event?: JSEvent, dataTarget?: String)
onCellRightClick(rowData: Object, columnId?: String, cellData?: Object, event?: JSEvent, dataTarget?: String)
onCellFocusGained(rowData: Object, columnId?: String, cellData?: Object, event?: JSEvent)
onColumnDataChange(rowindex: Number, columnindex: Number, oldvalue: Object, newvalue: Object, event?: JSEvent, rowData: Object): Boolean
onLazyLoadingGetRows(startRow: Number, endRow: Number, rowGroupCols: columnVO[], valueCols: columnVO[], pivotCols: columnVO[], pivotMode: Boolean, groupKeys: String[], filterModels: filterModelVO[], sortModels: sortModelVO[])
onColumnStateChanged(columnState: String, event?: JSEvent)
onRowGroupOpened(groupcolumnindexes?: Number[], groupkeys?: Object[], isopened?: Boolean)
onColumnFormEditStarted(rowindex?: Number, columnindex?: Number, value?: Object)
onDrop(sourceRows: Object[], targetRow: Object, event: JSDNDEvent)
onFooterClick(columnindex?: Number, event?: JSEvent, dataTarget?: String)
onCustomMainMenuAction(menuItemName: String, colId: String)
onReady()
datagrid / groupingtable
onCellClick(foundsetIndex: Number, columnIndex: Number, record: Object, event: JSEvent, dataTarget?: String)
onCellDoubleClick(foundsetIndex: Number, columnIndex: Number, record: Object, event: JSEvent, dataTarget?: String)
onCellRightClick(foundsetIndex: Number, columnIndex: Number, record: Object, event: JSEvent, dataTarget?: String)
onCellFocusGained(foundsetIndex: Number, columnIndex: Number, record: Object, event: JSEvent)
onColumnDataChange(foundsetIndex: Number, columnIndex: Number, oldvalue: Object, newvalue: Object, event?: JSEvent, record: Object): Boolean
onSort(columnindexes: Number[], sorts: String[])
onCellEditingStarted(foundsetIndex: Number, columnIndex: Number, value: Object, event?: JSEvent, record: Object)
onCellEditingStopped(foundsetIndex: Number, columnIndex: Number, oldvalue: Object, newvalue: Object, event?: JSEvent, record: Object)
onSelectedRowsChanged(isgroupselection: Boolean, groupcolumnid: String, groupkey: Object, groupselection: Boolean, event?: JSEvent)
onColumnStateChanged(columnState: String, event?: JSEvent)
onRowGroupOpened(groupcolumnindexes?: Number[], groupkeys?: String[], isopened?: Boolean)
onColumnFormEditStarted(foundsetIndex?: Number, columnindex?: Number, value?: Object)
onDrop(sourceRecords: Object[], targetRecord: Object, event: JSDNDEvent)
onFooterClick(columnindex?: Number, event?: JSEvent, dataTarget?: String)
onCustomMainMenuAction(menuItemName: String, colId: String)
onReady()
5. API Cheat-Sheet
Data
| Method | powergrid | datasettable | datagrid | groupingtable |
|---|
renderData(dataset, pks?) | yes | yes | — | — |
appendLazyRequestData(dataset, lastRowIndex?, pks?) | yes | yes | — | — |
newRows(rowsData[], appendToBeginning?) | yes | — | — | — |
updateRows(rowsData[]) | yes | — | — | — |
deleteRows(rowsKey[]) | yes | — | — | — |
notifyDataChange() | — | — | yes | yes |
refreshData() | — | — | yes | yes |
Columns
| Method | powergrid | datasettable | datagrid | groupingtable |
|---|
newColumn(id, index?) | yes | yes | — | — |
newColumn(dataprovider, index?) | — | — | yes | yes |
deleteColumn(id) | yes | yes | — | — |
removeColumn(index) | — | — | yes | yes |
removeAllColumns() | — | — | yes | yes |
getColumn(id, forChange?) | yes | yes | — | — |
getColumn(index) | — | — | yes | yes |
getColumnById(colId) | — | — | yes | yes |
getAllColumns() | yes | yes | — | — |
getViewColumns() | — | — | yes | yes |
getColumnsCount() | — | — | yes | yes |
getColumnIndex(colId) | — | — | yes | yes |
setColumns(columns[]) | — | — | yes | yes |
moveColumn(id, index) | yes | yes | yes | yes |
Selection
| Method | powergrid | datasettable | datagrid | groupingtable |
|---|
getSelectedRows() | yes | yes | — | — |
setSelectedRows(rowIndexes[]) | yes | yes | — | — |
getGroupedSelection() | — | — | yes | yes |
setGroupedSelection(records[]) | — | — | yes | yes |
getCheckboxGroupSelection() | — | — | yes | yes |
setCheckboxGroupSelection(groups[]) | — | — | yes | yes |
Editing
| Method | All variants |
|---|
editCellAt(rowindex/foundsetindex, columnindex) | yes (rowindex is 0-based for powergrid/datasettable; 1-based for datagrid/groupingtable) |
stopCellEditing(cancel?) | yes |
setFormEditorValue(value) | yes |
Layout & Display
| Method | powergrid | datasettable | datagrid | groupingtable |
|---|
autoSizeAllColumns() | yes | yes | yes | yes |
sizeColumnsToFit() | yes | yes | yes | yes |
setReadOnly(readonly, columnids[]?) | — | — | yes | yes |
requestFocus(columnindex) | — | — | yes | yes |
showToolPanel(show) | — | — | yes | yes |
isToolPanelShowing() | — | — | yes | yes |
Column State
| Method | powergrid/datasettable | datagrid/groupingtable |
|---|
getColumnState() | returns JSON string | returns JSON string |
restoreColumnState(state?) | restores; omit arg for design-time state | (state, onError?, columns?, filter?, sort?) — more control |
getColumnsFromState(state?) | — | yes |
setColumnsToState(columns[], state?) | — | yes |
Groups, Scroll, Export
| Method | powergrid | datasettable | datagrid | groupingtable |
|---|
getExpandedGroups() | yes | yes | yes | yes |
setExpandedGroups(groups) | yes | yes | yes | yes |
setFilterModel(filterModel) | — | — | yes | yes |
scrollToRow(rowData) (async) | yes | yes | — | — |
scrollToSelection() | — | — | yes | yes |
exportData(fileName?, ...) | yes | yes | — | — |
exportToDataset() | yes | yes | — | — |
isPivotMode() | yes | yes | — | — |
addAggCustomFuncs(aggFuncs) | yes | yes | — | — |
6. Func Properties (powergrid / datasettable)
These are client-side functions evaluated in the browser — no Servoy API access. Always wrap in an IIFE. Never use inline comments (they get inlined and break evaluation).
(function(idx, data, field, value, e) {
return data.status === 'active' ? 'row-active' : '';
})
(function(rowIndex, rowData, field, columnData, event) {
if (!columnData) return '';
return columnData > 100 ? 'cell-high' : 'cell-low';
})
(function(idx, data, field, value) {
return '<strong>' + value + '</strong>';
})
(function(idx, data, field) {
return data.editable === true;
})
(function(idx, data, field, params) {
return data[field] ? data[field].toUpperCase() : '';
})
(function(idx, data, field) {
return data.draggable === true;
})
(function(params) {
var label = params.node.key;
if (params.node.aggData) {
label += ' [count: ' + params.node.aggData.count + ']';
}
return label;
})
(function(src, dest, e, targetCell) {
return dest.id === 'targetId' ? 'move' : 'none';
})
(function(src, e) {
return '<div>Dragging ' + src.length + ' rows</div>';
})
datagrid / groupingtable uses dataproviders instead of func strings:
rowStyleClassDataprovider — calculation returning CSS class for the row
styleClassDataprovider — per-column calculation returning CSS class for cell
isEditableDataprovider — calculation returning non-zero to enable editing
dndSourceDataprovider — boolean dataprovider for drag allow/disallow
7. Common Implementation Patterns
Power Grid — Basic Setup
function onShow(firstShow, event) {
var ds = databaseManager.convertToDataSet(foundset, ['orderid', 'shipcountry', 'orderdate']);
elements.powergrid.renderData(ds, ['orderid']);
}
function onColumnDataChange(rowindex, columnindex, oldvalue, newvalue, event, rowData) {
var record = foundset.getRecord(rowData.orderid);
if (record) {
record[columnHeaders[columnindex]] = newvalue;
databaseManager.saveData(record);
}
return true;
}
Power Grid — CRUD (Angular powergrid only)
elements.powergrid.renderData(ds, ['id']);
elements.powergrid.newRows([{id: newId, name: 'New Item'}]);
elements.powergrid.updateRows([{id: existingId, name: 'Updated Name'}]);
elements.powergrid.deleteRows([{id: deletedId}]);
Power Grid — Lazy Loading
function onLazyLoadingGetRows(startRow, endRow, rowGroupCols, valueCols, pivotCols, pivotMode, groupKeys, filterModels, sortModels) {
var sql = 'SELECT id, name, amount FROM orders ORDER BY id LIMIT ? OFFSET ?';
var size = endRow - startRow;
var dataset = databaseManager.getDataSetByQuery('myserver', sql, [size, startRow], size);
var colNames = dataset.getColumnNames();
for (var i = 0; i < colNames.length; i++) {
dataset.setColumnName(i + 1, colNames[i].toLowerCase());
}
var nextRow = databaseManager.getDataSetByQuery('myserver', sql, [1, endRow], 1);
var lastRowIndex = -1;
if (nextRow.getMaxRowIndex() === 0) {
lastRowIndex = startRow + dataset.getMaxRowIndex();
}
elements.powergrid.appendLazyRequestData(dataset, lastRowIndex);
}
Inline Editing Setup
function onColumnDataChange(rowindex, columnindex, oldvalue, newvalue, event, rowData) {
if (newvalue === null || newvalue === '') {
return false;
}
return true;
}
Custom Form Editor
function onColumnFormEditStarted(rowindex, columnindex, value) {
forms.myEditorForm.elements.inputField.text = value;
}
function onSave() {
elements.powergrid.setFormEditorValue(forms.myEditorForm.elements.inputField.text);
elements.powergrid.stopCellEditing(false);
}
Column State Persistence
var state = elements.powergrid.getColumnState();
application.setUserProperty('gridState', state);
var state = application.getUserProperty('gridState');
if (state) {
elements.powergrid.restoreColumnState(state);
}
Row Grouping Setup
var groups = {
'USA': {
'California': {}
},
'Canada': {}
};
elements.datagrid.setExpandedGroups(groups);
var currentGroups = elements.datagrid.getExpandedGroups();
Pivot Mode (powergrid)
if (elements.powergrid.isPivotMode()) {
}
Custom Aggregation Functions (powergrid)
elements.powergrid.addAggCustomFuncs({
weightedAvg: '(function(values) { var sum = 0; for (var i = 0; i < values.length; i++) { sum += values[i]; } return sum / values.length; })'
});
Accessing Raw ag-Grid Behaviour via gridOptions / columnDef
Data Grid — Data Refresh
elements.datagrid.notifyDataChange();
elements.datagrid.refreshData();
function onDataBroadcast(dataSource, action, pks, cachedRecordWillChange) {
if (dataSource === 'orders') {
elements.datagrid.notifyDataChange();
}
}
Data Grid — Filter
elements.datagrid.setFilterModel({
'status': { filterType: 'text', type: 'equals', filter: 'active' },
'amount': { filterType: 'number', type: 'greaterThan', filter: 100 }
});
elements.datagrid.setFilterModel({});
Drag-and-Drop
function onDrop(sourceRows, targetRow, event) {
for (var i = 0; i < sourceRows.length; i++) {
}
}
8. CSS Selectors (datagrid / groupingtable)
| Selector | Description |
|---|
.ag-table | The whole table component |
.ag-table-info | The refresh icon |
.ag-table-info-notify | Refresh icon when notified of pending data changes (default: orange) |
.ag-header | Table header |
.ag-body | Table body |
.ag-header-cell | Individual header cell |
.ag-header-label | Label text inside header cell |
.ag-row | A table row |
.ag-row-even | Even rows |
.ag-row-odd | Odd rows |
.ag-row-selected | Selected row |
.ag-row-level-0 | Row at group level 0 (top-level group) |
.ag-row-level-1 | Row at group level 1 (nested group) |
.ag-row-level-x | Row at group level x — zero-indexed |
.ag-icon | Icons in the grid |
.ag-column-drop-cell | Droppable column in the row group panel |
.ag-column-drop-cell-text | Label inside droppable column |
.ag-column-drop-cell-button | Remove button inside droppable column |
.ag-table-header-tooltip | Column header tooltip |
.ag-table-cell-tooltip | Cell tooltip |
ag-Grid itself uses many more CSS custom properties and class selectors — see https://www.ag-grid.com/javascript-data-grid/theming-api/ for the full theming reference. The default theme is ag-theme-alpine for powergrid and can be set via the styleClass property.
9. Bug-Fixing Checklist
Before making any edit, read the relevant section of the implementation file to understand the current behaviour. Fix root cause, not symptoms.
| Symptom | Root cause | Fix |
|---|
updateRows / deleteRows silently does nothing | pks not passed to renderData | Call renderData(dataset, ['pkColumnName']) |
| Column property change has no effect | getColumn called without forChange=true | Use getColumn(id, true) before modifying |
onColumnDataChange rejects all edits | Handler returning false (or implicit undefined being treated as false in some paths) | Ensure handler returns true or nothing to accept; false to keep in edit mode |
| Angular edit has no effect in browser | npm run build not run after TypeScript change | Run npm run build in /aggrid/ |
| Column widths are ignored | columnsAutoSizing is set to SIZE_COLUMNS_TO_FIT which overrides explicit width | Set columnsAutoSizing to NONE or AUTO_SIZE, or remove it |
Lazy loading calls onLazyLoadingGetRows infinitely | lastRowIndex never passed to appendLazyRequestData | Detect last page and pass total row count as lastRowIndex |
scrollToRow has no immediate effect | It is async: true — must be called after grid is ready | Call inside or after onReady handler |
Cell click foundsetIndex is always -1 | Table is in grouping mode | This is by design — use record object pks to identify the row |
| Grouping breaks with multiple PKs | Data Grid does not support grouping with multiple PKs | Use a single PK foundset; this is a known limitation |
| Calculations/aggregations show wrong data in grouped columns | Not supported — grouped columns should not use calculations | Set enableRowGroup=false on calculation columns |
Func property (cellRendererFunc, etc.) does nothing | Inline JS comments inside the string break browser eval | Remove all // and /* */ comments from func strings |
gridOptions / columnDef setting has no effect | The ag-Grid option is marked Initial — only read at grid creation | The grid must be re-created; avoid relying on post-init changes for Initial options |
restoreColumnState does nothing on datagrid | If column IDs from saved state don't match current columns, restore is skipped by design | Ensure column id properties are stable and match between save and restore |
checkboxEditorValueServerToClientFunc / checkboxEditorValueClientToServerFunc not applied | datagrid-specific properties not set | Set these on the groupingtable/datagrid component to convert checkbox values |
10. ag-Grid Reference Links
Use these when gridOptions or columnDef are needed to access ag-Grid behaviour not exposed by the Servoy wrapper: