EditableDynamicListField
EditableDynamicListField manages a dynamic list of structured objects. Each item in the list is composed of named "slots" — configurable typed columns (text, long text, number, radio, object). In read mode items are displayed as compact read-only rows. Clicking the edit icon enables full item management: adding new items, editing each slot inline, removing items, and reordering via up/down arrows. On save the entire items array is persisted via API.
Props
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| name | string | Yes | — | Field name sent as the key in the update payload. |
| value | any[] | Yes | [] | Current array of item objects. |
| slots | Slot[] | Yes | — | Column/slot definitions. Each slot has type, name, optional label, and optional config. |
| updatePath | string | Yes | — | API endpoint path used to persist the change. |
| label | string | No | — | Label shown in the field header. |
| description | string | No | — | Helper text shown below the label. |
| canAddItems | boolean | No | true | When false, the "Add item" button is hidden. |
| canRemoveItems | boolean | No | true | When false, the delete button is hidden on each item. |
| breakpoint | number | No | 800 | Pixel width at which the layout switches to a stacked mobile layout. |
| forceMobileView | boolean | No | false | When true, always uses the stacked mobile layout regardless of screen width. |
| apiBaseUrl | string | No | — | Base URL prepended to updatePath. |
| useAuthToken | boolean | No | false | When true, the request includes the authorization token. |
| onChange | (items: any[]) => void | No | — | Called locally when the list changes. |
| onEditStart | () => void | No | — | Called when edit mode begins. |
| onEditSuccess | (updatedValue: any, newFormData: any) => void | No | — | Called after successful save. |
| onEditError | (error: any) => void | No | — | Called on API error; value is reverted. |
| onEditCancel | () => void | No | — | Called when the user cancels; value is reverted. |
| editIcon | string | No | "pencil" | Icon name for the edit button. |
| saveIcon | string | No | "check" | Icon name for the save button. |
| cancelIcon | string | No | "close" | Icon name for the cancel button. |
| containerStyle | React.CSSProperties | No | — | Style for the outermost wrapper. |
| headerStyle | React.CSSProperties | No | — | Style for the field header. |
| bodyStyle | React.CSSProperties | No | — | Style for the field body. |
| itemStyle | React.CSSProperties | No | — | Style applied to each item row. |
| labelStyle | React.CSSProperties | No | — | Style for the <label> element. |
| descriptionStyle | React.CSSProperties | No | — | Style for the description <p> element. |
Slot definition
interface Slot {
type: 'text' | 'longText' | 'number' | 'radio' | 'object' | 'boolean' | 'array';
name: string; // Object key for this slot's value
label?: string; // Column header label
config?: {
placeholder?: string;
inputStyle?: React.CSSProperties;
label?: string;
options?: { label: string; value: string }[]; // for 'radio'
showIf?: string; // Expression string to conditionally show the cell
};
}
Usage
Basic — product variants
import React, { useState } from 'react';
import EditableDynamicListField from '@/components/editable-fields/EditableDynamicListField';
const slots = [
{ type: 'text', name: 'size', label: 'Size', config: { placeholder: 'e.g. M' } },
{ type: 'text', name: 'color', label: 'Color', config: { placeholder: 'e.g. Red' } },
{ type: 'text', name: 'sku', label: 'SKU', config: { placeholder: 'e.g. PROD-M-RED' } },
];
export default function Example() {
const [variants, setVariants] = useState([]);
return (
<EditableDynamicListField
label="Variants"
name="variants"
value={variants}
slots={slots}
updatePath="/v1/products/7"
apiBaseUrl="https://api.example.com"
useAuthToken
onEditSuccess={(updated) => setVariants(updated)}
/>
);
}
With radio slot and conditional display
const slots = [
{
type: 'radio',
name: 'type',
label: 'Type',
config: {
options: [
{ label: 'Fixed', value: 'fixed' },
{ label: 'Percentage', value: 'percentage' },
],
},
},
{
type: 'text',
name: 'amount',
label: 'Amount',
config: { showIf: "type === 'fixed'" },
},
{
type: 'text',
name: 'percent',
label: 'Percent',
config: { showIf: "type === 'percentage'" },
},
];