A table with per-row Add or Remove action buttons used to incrementally build a list of selected items. Typically deployed as a pair: an add mode table shows available rows, a remove mode table shows what the user has picked.
Read Form Rules before using this component — keys, labels, required fields, expressions, visibility, and validation all follow shared conventions.
- Picking items from a table row-by-row using an Add/Remove action button per row — typical for selecting services, business activities, or records that require multiple visible attributes shown as columns
- Workflows that need a paired source table (
mode: "add") plus a selected-items table (mode: "remove") on the same form
- Selection lists driven by an integration gateway endpoint with server-side pagination and search
Instead of customitemselectiontable, use… |
For… |
customdatasettable |
Row click selection where the chosen row becomes a single form value (no Add/Remove pairing) |
customdatatable |
Pure read-only display of integration-gateway data with no selection |
customeditabletable |
Inline-editable grids where the user types into cells row-by-row |
custommultiselectdatafetch |
Simple dropdown-style multi-selection where only a label and value per item are needed |
customdropdowndatafetchpaginated |
Single-value pick from a large paginated integration-driven list |
The component operates in one of two modes set via selectionConfig.mode:
add mode — displays a source list; clicking the action button moves the row into selectionConfig.targetField (another field in the form that tracks selected items). Pair it with a remove mode table to show and manage selected items.
remove mode — displays the currently selected items (reads from its own formControl.value or props.sourceField); clicking the action button removes the row.
- API gateway — set
props.url + props.endpointCode + props.dataset to fetch from the integration gateway
- Field reference — set
props.sourceField to subscribe reactively to another form control's value
- Static array — set
props.data directly (rarely used in production)
- Remove mode self — in
remove mode with no other source, reads from own form control value
| Prop |
Type |
Required? |
Description |
columns |
array |
required |
Column definitions — each entry: { "key": "fieldInRow", "title": "Display Label", "sortable": false, "width": "auto" } |
selectionConfig |
object |
required |
Selection behaviour configuration (see below). |
url |
string |
required (API mode) |
API endpoint URL; always /integration/v1/fetch/sync for gateway calls. Must be on props, not inside dataset. |
endpointCode |
string |
required (API mode) |
Gateway endpoint code. Must be on props, not inside dataset. |
useBaseUrl |
boolean |
optional |
Prepend API gateway base URL to url; default true. |
dataset |
object |
required (API mode) |
Pagination and response mapping config (see below). |
payloadKey |
string |
optional |
Single form field key whose value is sent as payload (simple listener pattern). |
useMultiplePayloadKeys |
boolean |
optional |
If true, use payloadKeyMappings instead of payloadKey. |
payloadKeyMappings |
array |
optional |
Multi-payload mappings — each: { "key": "paramName", "sourceField": "formFieldKey" } or { "key": "paramName", "isStatic": true, "staticValue": "value" }. |
sourceField |
string |
optional |
Key of another form control to read data from reactively (alternative to API/static). |
data |
array |
optional |
Static row data array (lowest priority data source). |
searchable |
boolean |
optional |
Show search input above the table; default true. |
searchPlaceHolder |
string |
optional |
Placeholder text for the search input; default "Search". |
emptyMessage |
string |
optional |
Message when no rows are available; default "No items available". |
title |
string |
optional |
Heading rendered above the table. |
customTableClass |
string |
optional |
Extra CSS class on the <table> element. |
required |
boolean |
optional |
Always false. The required expression controls the runtime value. |
defaultRequired |
boolean |
optional |
Used with the conditional required expression pattern. |
errorMapping |
object |
optional |
Custom API error handling config. |
| Field |
Type |
Required? |
Description |
mode |
"add" | "remove" |
required |
"add" renders an Add button per row; "remove" renders a Remove button. |
targetField |
string |
required (add) |
Key of the form field where added items are stored. |
minItems |
number |
optional |
Minimum items that must be selected; fires minSelection validation error. |
maxItems |
number |
optional |
Maximum items allowed; disables the Add button once reached; fires maxSelection. |
preventDuplicates |
boolean |
optional |
Prevent adding the same row twice; default true. |
actionLabel |
string |
optional |
Text for the action column header and button. |
actionIcon |
string |
optional |
Icon class for the action button (e.g. "icon-plus", "icon-trash"). |
availableIcon |
string |
optional |
Icon shown when an item has not yet been added (add mode). |
selectedIcon |
string |
optional |
Icon shown when an item is already in targetField (add mode). |
removeIcon |
string |
optional |
Icon for the remove action (remove mode). |
displayMode |
"icon-only" | "icon-text" | "text-only" |
optional |
How the action button renders; default "icon-text". |
buttonStyle |
"default" | "icon-button" |
optional |
"default" outlined button; "icon-button" borderless icon link. |
| Field |
Type |
Required? |
Description |
dataField |
string |
optional |
Dot-notation path to the rows array in the API response. |
httpMethod |
"POST" | "GET" |
optional |
HTTP method; default "POST". |
params |
object |
optional |
Initial request params (e.g. { "page": 0, "size": 20 }). |
pageSize |
number |
optional |
Rows per page; default 20. |
pageStart |
number |
optional |
Starting page index sent to API; 0 for zero-based, 1 for one-based. |
pageKey |
string |
optional |
Request param key for the page number; default "page". |
sizeKey |
string |
optional |
Request param key for page size; default "size". |
searchKey |
string |
conditional |
Request param key for the search term (required when searchable: true with API data). |
totalPagesKey |
string |
optional |
Dot-notation path to total pages in the response; default "totalPages". |
totalElementsKey |
string |
optional |
Dot-notation path to total element count in the response. |
headers |
object |
optional |
Custom HTTP headers. |
searchLocal |
boolean |
optional |
If true, search filters client-side even when data came from API. |
| Key |
When it fires |
minSelection |
Selected count is below selectionConfig.minItems |
maxSelection |
Selected count exceeds selectionConfig.maxItems |
{
"key": "AVAILABLE_SERVICES",
"type": "customitemselectiontable",
"props": {
"label": "Available Services",
"url": "/integration/v1/fetch/sync",
"useBaseUrl": true,
"endpointCode": "GET_SERVICES_LIST",
"dataset": {
"dataField": "data.services",
"httpMethod": "POST",
"pageSize": 10,
"pageStart": 0,
"pageKey": "page",
"sizeKey": "size"
},
"columns": [
{ "key": "name", "title": "Service Name", "width": "70%" },
{ "key": "fee", "title": "Fee (RWF)", "width": "30%" }
],
"selectionConfig": {
"mode": "add",
"targetField": "SELECTED_SERVICES",
"actionIcon": "icon-plus",
"displayMode": "icon-only"
}
},
"validation": {
"messages": {
"minSelection": "Please select at least one service",
"maxSelection": "Maximum services reached"
}
}
}
Source table (add mode) paired with a remove table that shows selected items.
[
{
"key": "AVAILABLE_BUSINESS_ACTIVITIES",
"type": "customitemselectiontable",
"props": {
"label": "Select Business Activity",
"url": "/integration/v1/fetch/sync",
"useBaseUrl": true,
"endpointCode": "RDB_BUSINESS_ACTIVITY_LINES_FETCH",
"searchable": true,
"dataset": {
"dataField": "data.data",
"httpMethod": "POST",
"pageSize": 5,
"pageStart": 1,
"pageKey": "page",
"sizeKey": "size",
"searchKey": "searchKey",
"totalPagesKey": "data.totalPages",
"totalElementsKey": "data.totalElements",
"params": {
"page": 0,
"size": 5
}
},
"useMultiplePayloadKeys": true,
"payloadKeyMappings": [
{
"key": "businessSector",
"isStatic": true,
"staticValue": "A"
}
],
"columns": [
{ "key": "desEng", "title": "Activity", "width": "70%" },
{ "key": "code", "title": "Code", "width": "30%" }
],
"selectionConfig": {
"mode": "add",
"targetField": "SELECTED_BUSINESS_ACTIVITIES",
"maxItems": 3,
"actionIcon": "icon-plus",
"selectedIcon": "icon-check",
"displayMode": "icon-only",
"preventDuplicates": true
},
"emptyMessage": "No activities found",
"required": false,
"defaultRequired": false
},
"expressions": {
"props.hideField": "model.BUSINESS_SECTOR !== 'A'",
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"maxSelection": "You can select up to 3 activities only",
"minSelection": "Please select at least one activity to proceed"
}
}
},
{
"key": "SELECTED_BUSINESS_ACTIVITIES",
"type": "customitemselectiontable",
"props": {
"label": "Selected Activities",
"columns": [
{ "key": "desEng", "title": "Activity", "width": "70%" },
{ "key": "code", "title": "Code", "width": "30%" }
],
"selectionConfig": {
"mode": "remove",
"actionIcon": "icon-trash",
"displayMode": "icon-only"
},
"searchable": false,
"emptyMessage": "No activities selected yet"
}
}
]
- Putting
url and endpointCode inside dataset instead of at the top level of props — the component reads them from props directly; nesting them disables the fetch.
- Using
add mode without selectionConfig.targetField — selected items have nowhere to go; the selection is silently lost.
- Omitting
selectionConfig entirely — the component throws an error on init because selectionConfig is required.
- Setting
selectionConfig.maxItems without a maxSelection validation message — the Add button disables when the limit is reached but no error text appears.
- Setting
dataset.pageStart: 1 when the API expects 0-based pages — the first page is skipped.
- Not pairing an
add mode table with a remove mode table — the user can add items but has no way to undo selections.
- Using
searchable: true with API data but omitting dataset.searchKey — the search input appears but the keyword is never sent.