customeditabletable
An inline-editable tabular grid where each cell is a typed input (text, number, email, date, select); use for structured data with a fixed or user-managed list of rows and known columns.
An inline-editable tabular grid where each cell is a typed input (text, number, email, date, or select). Use when structured data has a fixed or user-managed list of rows and a known set of typed columns — such as product inventories, fee schedules, or employee contact tables.
Read Form Rules before using this component — keys, labels, required fields, expressions, visibility, and validation all follow shared conventions.
When to use
- Tabular data entry with a known set of typed columns (text, number, email, date, select)
- Fixed rows known at design time (e.g. a list of product categories the user fills in)
- User-managed rows the officer can add and delete at runtime (e.g. fee schedules, line items)
- Grids that need column-level validation (
required,min/max,pattern) without rendering full Formly fields per cell
When NOT to use
Instead of customeditabletable, use… | For… |
|---|---|
customdatatable | Read-only display of integration-gateway data |
customdatasettable | Read-only display of model data or direct URL fetches; row click selection |
customitemselectiontable | Per-row Add/Remove selection workflows |
customeditableinput | A single inline-editable cell input used as a column primitive (not a full table) |
customcascadingeditableinput | A cascading cell input where one column's options depend on another column's value within a row |
| A regular Formly repeater | Rows with deeply nested or heterogeneous fields that don't fit a uniform cell grid |
Props
Component-level props
| Prop | Type | Required? | Description |
|---|---|---|---|
label | string | optional | Label displayed above the table. |
rows | array | required | Array of row configurations — see row shape below; omit content when allowRowAddition: true (user creates rows). |
columns | array | required | Array of column configurations — see column shape below. |
rowColumnTitle | string | optional | Header text for the row-label column; only shown when showRowLabels: true. |
showRowLabels | boolean | optional | When true, displays a leading column with each row's label. |
allowRowAddition | boolean | optional | When true, shows an "Add Row" button so the user can append rows at runtime; default false. |
allowRowDeletion | boolean | optional | When true, shows a delete button on each row; default false. |
disabled | boolean | optional | When true, all cell inputs are read-only. |
customTableClass | string | optional | CSS class applied to the table element for custom styling. |
Row configuration (each item in rows)
| Key | Type | Required? | Description |
|---|---|---|---|
key | string | required | Unique identifier for the row — used as the key in the returned data object. |
label | string | required | Display label shown in the row-label column. |
disabled | boolean | optional | When true, all inputs in this row are disabled. |
Column configuration (each item in columns)
| Key | Type | Required? | Description |
|---|---|---|---|
key | string | required | Unique identifier for the column — used as the field name in each row's data. |
label | string | required | Column header text. |
type | string | optional | Cell input type: "text" (default), "number", "email", "date", "select". |
required | boolean | optional | When true, the cell must not be empty. |
disabled | boolean | optional | When true, all cells in this column are read-only. |
placeholder | string | optional | Placeholder text shown inside the cell input. |
width | string | optional | CSS width for the column — e.g. "150px" or "20%". |
min | number | optional | Minimum value — number type only. |
max | number | optional | Maximum value — number type only. |
pattern | string | optional | Regex validation pattern — text type only. |
options | array | required (select) | For select type — array of { "label": string, "value": any }. |
Column types
| Type | Input widget | Notes |
|---|---|---|
"text" | Text input | Supports pattern validation |
"number" | Numeric input | Supports min / max; value stored as number |
"email" | Email input | Built-in email format validation |
"date" | Date picker | Returns ISO date string (YYYY-MM-DD) |
"select" | Searchable dropdown | Requires options array |
Data structure
The component returns an object keyed by row.key, each holding an object keyed by column.key:
{
"row1": { "quantity": 100, "price": 5000, "status": "available" },
"row2": { "quantity": 50, "price": 7500, "status": "out_of_stock" }
}Examples
Fixed rows, two number columns
A simple inventory table with fixed rows and no add/delete.
{
"key": "INVENTORY",
"type": "customeditabletable",
"props": {
"label": "Product Inventory",
"rowColumnTitle": "Product",
"showRowLabels": true,
"rows": [
{ "key": "PRODUCT_1", "label": "Laptop" },
{ "key": "PRODUCT_2", "label": "Mouse" }
],
"columns": [
{
"key": "QUANTITY",
"label": "Quantity",
"type": "number",
"required": true,
"min": 0
},
{
"key": "PRICE",
"label": "Price (RWF)",
"type": "number",
"required": true,
"min": 0
}
]
},
"validation": {
"messages": {
"required": "This field is required."
}
}
}Dynamic rows with mixed column types
A fee schedule where officers add and delete rows; includes a text column with regex, a number column, and a status select.
{
"key": "FEE_SCHEDULE",
"type": "customeditabletable",
"props": {
"label": "Fee Schedule",
"allowRowAddition": true,
"allowRowDeletion": true,
"rows": [],
"columns": [
{
"key": "SERVICE_NAME",
"label": "Service Name",
"type": "text",
"required": true,
"placeholder": "Enter service name",
"width": "40%",
"pattern": "^[a-zA-Z0-9\\s\\-]+$"
},
{
"key": "FEE",
"label": "Fee (RWF)",
"type": "number",
"required": true,
"min": 0,
"placeholder": "Enter fee",
"width": "25%"
},
{
"key": "STATUS",
"label": "Status",
"type": "select",
"required": true,
"width": "25%",
"options": [
{ "label": "Active", "value": "ACTIVE" },
{ "label": "Inactive", "value": "INACTIVE" }
],
"placeholder": "Select status"
}
]
},
"validation": {
"messages": {
"required": "This field is required.",
"min": "Value must be 0 or greater.",
"pattern": "Service name contains invalid characters."
}
}
}Common mistakes
- Setting
rows: []whenallowRowAddition: false— the table renders with no rows and the user has no way to add any; either provide arowsarray with at least one item or setallowRowAddition: true. - Omitting
optionson aselect-type column — the dropdown renders empty. - Using
min/maxon atext-type column — these constraints only apply tonumbertype; they are silently ignored. - Using
patternon anumber-type column —patternonly applies totext; useminandmaxfor numeric constraints. - Setting
required: trueat the component level —requiredon the table itself is not supported; mark individual columns asrequired: true. - Omitting
rowColumnTitlewhenshowRowLabels: true— the row-label column header is blank. - Reusing the same
keyacross two columns — the returned data object overwrites one column's value; every columnkeymust be unique within the table.
Checklist
-
keyisUPPER_SNAKE_CASEand unique across the entire form -
props.labelis present, unique, and correctly cased - Every
columns[].keyandrows[].keyis unique within the table -
select-type columns include anoptionsarray -
min/maxare only used onnumbercolumns -
patternis only used ontextcolumns - Cell-level
required: truehas a matchingvalidation.messages.required -
min/max/patternconstraints each have matching validation messages -
allowRowAddition: falseand a non-emptyrowsarray, orallowRowAddition: true(never empty rows with no add button) -
showRowLabels: trueis paired with arowColumnTitle - Not using
customeditabletablewhen the data is read-only (usecustomdatatableorcustomdatasettable)
customdynamicrepeater
An API-driven repeater that creates one row per item returned from a fetch. The user fills in the visible fields per row but does not control row count.
customexpansionrepeater
A repeater whose rows render as collapsible expansion panels. Use when each row contains many fields and a flat list would overwhelm the page.