Skip to main content

Using Table

Configuring Avatar Columns

Simple Column Displaying Avatar Alone

If the Table's data source form has an upload field, simply set the Table column type to "Avatar". The configuration and result are shown below:

The form upload avatar field ID can be anything — it doesn't have to be avatar. Set it as needed.

avatar-form-upload

Table column settings

avatar-upload-column

Runtime result

avatar-upload-column-view

Merging Avatar and Name into One Column

In most cases, the avatar is not displayed as a standalone column. Instead, it looks like the following:

avatar-label-column-view

To achieve this, a data transformation is required. The Avatar column expects data in the following format:

{ 
"avatar": "image URL",
"label": "display label"
}

For example, the data transformation configuration for the above result is:

avatar-label-column

avatar-label-column-js

The JS data transformation code is as follows:

(function transform(data) {
return data.map((item) => {
return {
...item,
nickName: {
avatar: item.avatar?.resources?.[0]?.url,
label: item.nickName,
},
};
});
})(this);

Multi-Select Avatar Group

When there are multiple avatars, you can use the group display format. In this case, the Avatar column expects data in array format:

[
{
"avatar": "image URL",
"label": "display label"
},
{
"avatar": "image URL",
"label": "display label"
}
]

For example, here is a multi-select upload component where multiple images are displayed as multiple avatars. The configuration and runtime result are shown below:

Table column settings

avatar-group-column

Data transformation

avatar-group-js

The JS data transformation code is as follows:

(function transform(data) {
return data.map((item) => {
const c817f839 = item.c817f839?.resources?.map((x) => {
return { avatar: x.url, label: item.name };
});
return { ...item, c817f839 };
});
})(this);

Runtime result

avatar-group-view

Merged Row Selection Column (GROUP_SELECT_COLUMN)

warning

GROUP_SELECT_COLUMN is similar to AG Grid's native Row Grouping feature, but they are not the same and cannot be used together. When the merged row selection column is enabled, AG Grid's row grouping functionality is automatically disabled.

GROUP_SELECT_COLUMN is a special table column type that merges adjacent rows with identical values into a single cell, displaying a checkbox in the merged cell to enable "select entire group" interaction.

Typical use case: A field in the data (such as order number or batch number) corresponds to multiple detail rows, and users need to batch select/deselect these rows by group.

The runtime effect is shown below — multiple rows are merged and share a single checkbox:

group-select-column-1

Quick Start

In the form designer, select a table column and switch the column type to "Merged Row Selection". The designer will automatically complete the following configurations:

  1. Switch the table to clientSide (full data load) mode
  2. Enable cell merging
  3. Disable pagination
  4. Hide the grouping panel
  5. Configure multi-select behavior (hide AG Grid's built-in checkbox column, with this column taking over selection logic)

Users only need to configure the grouping field to start using it.

Add a new column in the designer, select "Merged Row Selection", then configure the data field:

group-select-column-2

group-select-column-3

group-select-column-4

Configuration Options

OptionDescriptionDefault
Grouping Field (field)Adjacent rows with the same value will be merged into a group sharing a single checkbox. Supports selecting from the current data source's field list or manually entering any field path.
Column ID (colId)Unique identifier for the column.'group-selection'
Sort (sort)Sort direction to ensure grouped data is adjacent, which is required for merging to work correctly.Ascending
Don't Merge Empty Values (notMergeEmptyValue)When checked (default): empty values remain independent and won't be merged with other empty value rows. When unchecked: empty values will also be merged into a group.Checked (empty values not merged)

Runtime Behavior

Grouping and Merging Logic

  1. After the table loads data, it sorts by the grouping field in the configured sort direction to ensure grouped data is adjacent.
  2. Adjacent rows with the same grouping field value are merged into a single cell. Rows with null / undefined / '' (empty values) remain independent by default, unless "Don't Merge Empty Values" is unchecked.

Checkbox Selection Logic

  • Cell checkbox: Checking/unchecking selects or deselects all selectable rows within that group.
  • Header checkbox: Selects/deselects all selectable rows after filtering (regardless of grouping).
  • Indeterminate state: When only some rows in a group are selected, the cell checkbox displays an indeterminate state.

JSON Configuration Example

Below is a complete column configuration example (columnDefs section):

{
"columnDefs": [
{
"type": "GROUP_SELECT_COLUMN",
"colId": "group-selection",
"field": "orderNo",
"sort": "asc",
"width": 60,
"minWidth": 60,
"flex": null
},
{
"field": "orderNo",
"headerName": "Order No"
},
{
"field": "itemName",
"headerName": "Item Name"
},
{
"field": "quantity",
"headerName": "Quantity"
}
]
}
tip

When switching column type via the designer, table-level configurations (clientSide mode, disabled pagination, etc.) are set automatically — no manual configuration needed. If writing JSON manually, you also need to configure rowModelType: "clientSide", enableCellSpan: true, pagination: false, rowGroupPanelShow: "never", and rowSelection in the table's componentProps.

Getting Selected Row Data

Used with a button to retrieve user-selected row data for further operations (such as batch approval).

Implementation steps:

  1. Set an ID for the table (e.g., my-table), which will be used in JS to access the table API.
  2. Add a button next to the table and set the button's action to call a custom method (e.g., approve).
  3. In JS, register the method via formApi.registerMethod. Inside the method, use formApi.getFieldApi to get the table's AG Grid API, then call getSelectedRows() to get the selected rows.

Button configuration in the designer for calling a custom method:

group-select-column-5

formApi.registerMethod('batchApprove', (params) => {
const selectedRows = formApi.getFieldApi('my-table').gridApi.getSelectedRows();
// selectedRows is an array of all currently selected row data
// Perform next steps here, such as calling an API for batch approval
console.log('Selected rows:', selectedRows);
});

Runtime effect after clicking the button to get selected row data:

group-select-column-6

CSS Style Customization

The component dynamically adds CSS classes to merged cells at runtime for easy style customization:

Available Classes

Class NameApplied WhenDescription
icp-group-select-cell-selectedAny row in the group is selectedControls the selected background color of merged cells
icp-group-select-evenGroup index is even (0, 2, 4...)Used to visually distinguish adjacent groups
icp-group-select-oddGroup index is odd (1, 3, 5...)Used to visually distinguish adjacent groups

Custom Even/Odd Group Styles Example

Use icp-group-select-even / icp-group-select-odd to add different visual distinctions between adjacent groups, such as different colored right borders:

/* Even groups: blue right border */
.icp-group-select-even {
border-right: 3px solid #1890ff !important;
}

/* Odd groups: green right border */
.icp-group-select-odd {
border-right: 3px solid #52c41a !important;
}

Customizing via columnDef cellClass

If you only want to customize styles for a specific table's grouped selection column, you can use cellClass in the column configuration JSON:

{
"type": "GROUP_SELECT_COLUMN",
"colId": "group-selection",
"field": "orderNo",
"cellClass": "my-custom-group-cell"
}

Combined with even/odd group classes to write corresponding CSS styles:

/* Even groups: blue right border */
.my-custom-group-cell.icp-group-select-even {
border-right: 3px solid #1890ff !important;
}

/* Odd groups: green right border */
.my-custom-group-cell.icp-group-select-odd {
border-right: 3px solid #52c41a !important;
}

Where to Configure CSS

The custom CSS above can be configured in the frontend appearance settings. For detailed instructions, please refer to: Appearance - CSS Customization.

Notes and Limitations

  1. Only supports full data load (clientSide) mode
    Merged Row Selection relies on client-side APIs for header select-all and continuous group detection. It does not support serverSide (server-side pagination/sorting) mode. When switching to this column type, the table automatically switches from serverSide to clientSide.

  2. Pagination must be disabled
    Pagination splits data across pages, which may cause rows in the same group to be on different pages, making cross-page merging impossible.

  3. Sorting and grouping of other columns are disabled
    Manual sorting or grouping of other columns breaks the sort continuity of the grouping field, causing merging to fail. Therefore, when this column is enabled, sorting and grouping for other columns are automatically disabled.

  4. Performance considerations
    Since clientSide mode must be used with pagination disabled, all data is loaded to the frontend at once. This may affect performance with large datasets (e.g., tens of thousands of rows). Please evaluate based on your actual data volume.

  5. Empty value handling
    By default, rows with empty grouping field values (null / undefined / '') remain independent and are not merged. To allow empty value rows to participate in merging, uncheck "Don't Merge Empty Values".

  6. Sort direction purpose
    The sort configuration (ascending/descending) primarily ensures grouped data is adjacent. Choose ascending or descending based on which groups you want to appear first.

Complex Table Column Definitions (CUSTOM_COLUMN)

warning

Do not overuse CUSTOM_COLUMN. Since each custom column requires recursive rendering of a low-code component tree, excessive usage may cause performance issues. Only use CUSTOM_COLUMN when none of the other built-in column types (such as TEXT_COLUMN, SELECT_COLUMN, AVATAR_COLUMN, etc.) can meet your requirements.

When built-in column types cannot meet your requirements, you can use the CUSTOM_COLUMN type to customize table column rendering. CUSTOM_COLUMN allows you to describe the cell rendering structure directly using low-code JSON configuration within the column definition, without writing any JS code.

For example, the red-outlined area in the image below shows 2 Text components concatenated + 1 Button action.

custom-column-3

Configuration Example

The following example demonstrates a custom column that displays text and conditionally shows different icon buttons based on row data:

custom-column-1

custom-column-2

{
"type": "CUSTOM_COLUMN",
"colId": "customCol",
"headerName": "Custom Cell",
"width": 250,
"flex": null,
"cellRendererParams": {
"fields": [
{
"component": "Stack",
"componentProps": {
"flexDirection": "row",
"alignItems": "center"
},
"fields": [
{
"component": "Text",
"componentProps": {
"content": ":textCol :statusCol4"
}
},
{
"component": "Button",
"componentProps": {
"icon": "material:star-border-rounded",
"action": {
"type": "confirm"
},
"type": "text",
"size": "small"
},
"hidden": {
"dataPredicate": {
"operator": "AND",
"conditions": [
{
"condition": "equals",
"value": "done",
"dataField": "statusCol4"
}
]
}
}
},
{
"component": "Button",
"componentProps": {
"icon": "material:star-rounded",
"action": {
"type": "confirm"
},
"type": "text",
"size": "small"
},
"hidden": {
"dataPredicate": {
"operator": "AND",
"conditions": [
{
"condition": "equals",
"value": "done",
"dataField": "statusCol4"
}
]
},
"valueIfPositive": false,
"valueIfNegative": true
}
}
]
}
]
}
}

The example above produces the following behavior:

  • Displays concatenated text from the current row's textCol and statusCol4 fields
  • When statusCol4 is not equal to "done", shows an outlined star button (star-border)
  • When statusCol4 equals "done", shows a filled star button (star)

How It Works

The CUSTOM_COLUMN column accepts a set of low-code component JSON configurations via cellRendererParams.fields, and renders them recursively using RecursionRenderer. During rendering, the current row data is injected into the context, allowing child components to reference current row field values using the :fieldName variable syntax.

JSON Configuration

In the table column definition, set type to CUSTOM_COLUMN and define the component structure to render in cellRendererParams.fields:

{
"type": "CUSTOM_COLUMN",
"colId": "customCol",
"headerName": "Custom Column",
"width": 250,
"cellRendererParams": {
"fields": [
// Low-code component JSON configuration
]
}
}

Key Properties:

PropertyDescription
typeFixed value "CUSTOM_COLUMN"
colIdUnique identifier for the column
headerNameColumn header title
cellRendererParams.fieldsLow-code component JSON array defining cell render content
info

CUSTOM_COLUMN defaults to sortable: false and filter: false, since custom-rendered columns typically do not support sorting and filtering.

Referencing Current Row Data

In the fields configuration, you can reference current row field values using the :fieldName syntax. For example, if the current row data is { "textCol": "Hello", "statusCol4": "done" }:

  • :textCol resolves to "Hello"
  • :statusCol4 resolves to "done"
  • Concatenation is also supported, e.g., ":textCol :statusCol4" resolves to "Hello done"

Use Cases

CUSTOM_COLUMN is suitable for the following scenarios:

  • Combining multiple components in a cell (e.g., text + button + icon)
  • Conditionally rendering different content based on row data (via hidden + dataPredicate)
  • Placing interactive components in cells (e.g., buttons triggering actions)
  • Customizing cell layout (via layout components like Stack)
tip

CUSTOM_COLUMN also applies to the EditableTable component.

Dynamically Modifying Table Columns

The Table API provides the updateComponentProps method, which can be used to modify any table properties, including the column definitions (columnDefs).

As shown in the example below, after the table is rendered, clicking the "Add Column" button dynamically appends a new column 12/2/2024 to the table.

update-column-defs-run

The configuration in the designer is as follows:

update-column-defs-button update-column-defs-js

The JavaScript code for this example is as follows:

formApi.registerMethod('changeColumnDefs', () => {
const tableApi = formApi.getFieldApi('45cfdeaa-table');
const { columnDefs } = tableApi.getComponentProps();

const newColDef = {
colId: new Date().toLocaleDateString(),
field: new Date().toLocaleDateString(),
headerName: new Date().toLocaleDateString(),
};
const newColumnDefs = [...columnDefs, newColDef];

tableApi.updateComponentProps({
columnDefs: newColumnDefs,
});
});

Saving External Filter Conditions to Table Views

This example addresses the scenario where you don't want to use the table toolbar's built-in filter functionality, but instead configure filter conditions externally. At the same time, you want to store these filter conditions in the table's state (like the built-in filter does) so they persist across page refreshes. Additionally, you want to save external filter conditions to table views for easy switching.

As shown below, the top-left has a custom "feature" filter condition. Changing the feature refreshes the table's filter request. The condition is also saved to a view named "view 2" in the table toolbar's top-right corner. Switching to "view 2" next time will automatically restore the feature filter condition.

custom-out-filter-1

The implementation steps are as follows:

Linking External Filter Conditions to the Table

In this example, an external filter condition ACL with ID feature is configured. The table's ID is user-story-list-table.

custom-out-filter-2

custom-out-filter-3

Then, configure the table's data source filter to reference a dynamically changing form data field. In this example, the form data field is called :formData.customUserStoryFilters. The name customUserStoryFilters can be changed to anything you prefer, as long as it matches the key used in the JS code below.

custom-out-filter-4

Use the method formApi.on('fieldValueChange', (id, newValue) => {}) to listen for changes in the external custom field value.

When the external custom field value changes, call formApi.setValue('customUserStoryFilters', dataFilters); to set the table's bound data filter customUserStoryFilters.

Storing External Filter Conditions in Table State via JS

Use the table API tableApi.setTableState() to save external filter conditions to the table's state.

Then use the table event onSwitchFavoriteView to listen for view switching operations in the table toolbar's top-right corner, and retrieve the previously saved customUserStoryFilters.

The complete JS code for both steps is as follows:

function customFilterModelToApiDataFilters(customFilterModel) {
if (!customFilterModel) return null;
return customFilterModel.map((item) => {
if (item.filterType === 'set') {
return {
...item,
// The backend API filters by label, so send labels
values: item.values.map((v) => v.label),
};
}
return item;
});
}

function setCustomFilterToTable(customFilterModel) {
// Convert to the filter format required by the API
const dataFilters = customFilterModelToApiDataFilters(customFilterModel);
// Set to the table's componentProps.dataFilters dependency, the table will automatically refresh data requests
formApi.setValue('customUserStoryFilters', dataFilters);
}

function restoreCustomFilter(customFilterModel) {
// Set as needed — set as many fields as required. For many fields, use formApi.setData() for batch setting
const feature = (customFilterModel || []).find((item) => item.colId === 'feature');
if (feature) {
formApi.setValue('feature', feature.values);
} else {
formApi.setValue('feature', null);
}

setCustomFilterToTable(customFilterModel);
}

function initialCustomFilter() {
const tableApi = formApi.getFieldApi('user-story-list-table');

// If your table exists when the page first loads, simply get the customFilterModel saved from the last page visit via tableApi
const customFilterModel = tableApi.getTableState().customFilterModel;
// If your table is hidden when the page first loads, retrieve it from your own localStorage
// Example: const customFilterModel = JSON.parse(localStorage.getItem(customFilterKey));

restoreCustomFilter(customFilterModel);
}

formApi.setFieldComponentProps('user-story-list-table', {
// Listen for table view switching to apply the custom customFilterModel
onSwitchFavoriteView: (view) => {
restoreCustomFilter(view.customFilterModel);
},
});

formApi.on('ready', function () {
// If customFilterModel was saved from the last page visit, retrieve it and set it to the custom filter fields and the table's dataFilters property
initialCustomFilter();
});

formApi.registerMethod('doFilter', () => {
const formData = formApi.getData();
const { feature } = formData;

const customFilterModel = [];
if (feature?.length) {
customFilterModel.push({
colId: 'feature',
filterType: 'set',
// Save the complete value-label array for restoring ACL display on page refresh
values: feature,
});
}

setCustomFilterToTable(customFilterModel);

// Save the custom filter to tableState so it can be saved with table views,
// retrieved on page refresh via tableApi.getTableState(), and obtained during view switching via onSwitchFavoriteView
const tableApi = formApi.getFieldApi('user-story-list-table');
tableApi.setTableState({
// The name can be anything — here it's called customFilterModel
customFilterModel: customFilterModel,
});

// For the case where the table doesn't exist initially, store a copy in localStorage if needed.
// IMPORTANT: The key must be stored per user, otherwise all users will see the same data.
// You can follow the table's default localStorage key pattern. Use the following code to get the username and pbc token:
// const { pbcToken, userProfile } = formApi.getContext();
// const customFilterKey = ['table', pbcToken, 'optionally add formEntityToken and layoutToken here', 'your-table-id', userProfile.username].join('.');
// localStorage.setItem(customFilterKey, JSON.stringify(customFilterModel));
});

API

For the complete Table API reference, please refer to Table