Adaptable Forms
Summary
- Adaptable Forms are widely used in AdapTable and are highly configurable and flexible
- They dynamically display form data and are widely used including Export, Alerts and Data Sets
There are a few places in AdapTable where a custom form can be displayed, including:
- Custom Export Destinations
- Alert Messages
- Data Sets
- Custom Toolbars - inline form controls rendered directly in a Dashboard Custom Toolbar
- Row Forms - the Create / Clone / Edit Row dialogs, with per-column overrides via
rowFormField
AdapTable provides an Adaptable Form to make displaying form data easy.
Adaptable Forms are primarily made up of 3 collections:
- Title and Description
- Form Fields
- Adaptable Buttons
Adaptable Form Object
Adaptable Forms are defined using the AdaptableForm object, defined as follows:
| Property | Type | Description | Default |
|---|---|---|---|
| buttons | AdaptableButton<T>[] | Buttons to include in the Form | |
| description | string | Additional information to appear in the Form | |
| fields | (AdaptableFormField|AdaptableFormField[] |AdaptableFormFieldGroup)[] | Collection of Dynamic Fields and Field Groups to display. | |
| layout | AdaptableFormLayout | How the form's fields are arranged on screen. | 'rows' |
| onSubmit | (formData: TData, context: T) => void | Optional form-level submit hook. | |
| title | string | Title to appear in the Form |
Title & Description
The form object has 2 string properties used to display information about the form:
title- text shown at the top of the Formdescription- additional text showing information the form requires and displayed beneath the title
Fields
A Form can contain as many fields as required.
All fields are of type AdaptableFormField which is defined as follows:
| Property | Type | Description | Default |
|---|---|---|---|
| clearToDefault | boolean | For single select fields only. When the user clears the combobox, whether to restore instead of leaving the field empty. | |
| defaultValue | string | boolean | number | Array<string | number | boolean> | Field Default Value - can be of type string, boolean, number, or for select fields with multi: true an array of those. | |
| disabled | boolean | ((formData:AdaptableFormData, context:BaseContext) => boolean) | Disable the field's input. Either a boolean or a function evaluated against the current form data. Disabled fields render but the user cannot edit them. | |
| fieldType | AdaptableFormFieldType | Field Type - dictates which UI control is rendered | |
| helpText | string | Inline help text rendered immediately below the field's input. Useful for short explanations such as "format: YYYY-MM-DD" or "min 3 chars". | |
| hidden | boolean | ((formData:AdaptableFormData, context:BaseContext) => boolean) | Hide the field. Either a boolean or a function evaluated against the current form data. Hidden fields are not rendered and are excluded from validation. | |
| label | string | Label to display in the Field | |
| max | number | string | Maximum allowed value. For number and slider: the highest accepted number. For date, time and datetime: the latest allowed value in ISO format. | |
| maxLength | number | Maximum length for text and textarea fields. | |
| min | number | string | Minimum allowed value. For number and slider: the lowest accepted number. For date, time and datetime: the earliest allowed value in ISO format (YYYY-MM-DD / HH:mm / YYYY-MM-DDTHH:mm). | |
| minLength | number | Minimum length for text and textarea fields. | |
| multi | boolean | For select fields only. When true the field renders as a multi-select combobox; the field's value is an array of selected option.values rather than a single value. | false |
| name | string | Name of the Field | |
| onValueChange | (value: any, context:BaseContext) => void | Optional callback invoked whenever value of this field changes | |
| options | AdaptableFormFieldOption[] | ((formData:AdaptableFormData, context:BaseContext) =>AdaptableFormFieldOption[] | Promise<AdaptableFormFieldOption[]>) | Items to populate the select and radio fieldTypes. | |
| pattern | string | Regular expression that the value of a text field must match. | |
| placeholder | string | Placeholder text shown inside text, textarea, number, date, time and datetime inputs when empty. For select fields it is rendered as the empty-state label when no value is selected. | |
| render | (params:AdaptableFormFieldRenderParams) => React.ReactNode | For custom fields only. Renders an arbitrary React node as the field's input. Receives the current value, a setValue setter (controlled), the current form data, the Adaptable context and the resolved disabled state. | |
| required | boolean | If true the field is treated as required. An empty value ('', null, undefined, or unchecked checkbox) will produce a validation error. | |
| rows | number | For textarea fields: the visible number of text rows. | 3 |
| step | number | Stepping interval for number, slider, time and datetime fields. | |
| tooltip | string | Tooltip shown when hovering the field's label. | |
| validate | (value: any, formData:AdaptableFormData, context:BaseContext) => string | null | undefined | Custom field-level validation. Return an error message string to mark the field invalid, or null/undefined if the value is valid. |
Field Types
The fieldType property describes which UI control to display for the field.
It is of type AdaptableFormFieldType and can be any of:
fieldType | UI control |
|---|---|
text | Single-line text input |
textarea | Multi-line text input - configurable rows (defaults to 3) |
number | Numeric input |
slider | Range slider with min/max/step plus an inline value read-out - see Slider |
date | Native HTML5 date picker |
time | Time-of-day picker (HH:mm) |
datetime | Combined date + time picker |
color | Native HTML5 colour picker (value is a #RRGGBB string) |
select | Combobox populated from options (single or multi - see Multi-Select) |
radio | Mutually-exclusive group of radio buttons populated from options - see Radio Group |
checkbox | Checkbox |
textOutput | Read-only inline label - displays the field's current value as plain text (useful for static guidance or computed labels alongside other controls) |
custom | Renders any controlled React node via a render function - see Custom Fields |
Hint
If the field type is 'select' or 'radio', the options property can be used to populate what appears in the dropdown / radio group
Textarea
A textarea field renders a multi-line text input. The rows property controls the visible height (default 3); minLength and maxLength are honoured for validation.
{
name: 'notes',
label: 'Notes',
fieldType: 'textarea',
rows: 5,
maxLength: 500,
placeholder: 'Internal notes...',
}Note
Pressing Enter inside a textarea inserts a newline rather than submitting the form, even when onSubmit is set. Use Shift + Enter if you want both behaviours alongside.
Slider
A slider field renders a range input with min / max / step plus a tabular value read-out next to it. The current value is always a number.
{
name: 'opacity',
label: 'Opacity',
fieldType: 'slider',
defaultValue: 0.5,
min: 0,
max: 1,
step: 0.05,
}Radio Group
A radio field renders all options as a row of mutually-exclusive radio buttons. Use it instead of a select when there are only a handful of options and you want them all visible at once.
{
name: 'priority',
label: 'Priority',
fieldType: 'radio',
defaultValue: 'normal',
options: [
{ label: 'Low', value: 'low' },
{ label: 'Normal', value: 'normal' },
{ label: 'High', value: 'high' },
],
}Time, Datetime & Colour
The time, datetime and color field types wrap the corresponding native HTML5 inputs:
time- the value is anHH:mm(orHH:mm:sswhenstepis sub-minute) stringdatetime- combined date + time, value isYYYY-MM-DDTHH:mmcolor- HTML5 colour picker, value is a#RRGGBBstring
{ name: 'reminderAt', label: 'Reminder', fieldType: 'time' },
{ name: 'startsAt', label: 'Starts', fieldType: 'datetime' },
{ name: 'tagColour', label: 'Tag', fieldType: 'color', defaultValue: '#1e88e5' },Multi-Select
A select field can render as a multi-select by setting multi: true. The field's value becomes an array of selected option.values rather than a single value:
{
name: 'languages',
label: 'Languages',
fieldType: 'select',
multi: true,
defaultValue: [],
options: [
{ label: 'JavaScript', value: 'JavaScript' },
{ label: 'TypeScript', value: 'TypeScript' },
{ label: 'Python', value: 'Python' },
],
}Hint
For multi-selects the required validation requires at least one selected option, and an empty array ([]) - not an empty string - is treated as the empty state
Read-only output (textOutput)
A textOutput field is read-only: it echoes formData[field.name] as plain text with no input control. It never participates in validation (even if required were set, it would be ignored). Use it for static guidance, computed summaries, or labels that sit next to editable fields—often combined with multiple fields on one row.
{
name: 'summaryLine',
label: 'Summary',
fieldType: 'textOutput',
},
// elsewhere in your host code, keep `formData.summaryLine` in syncDynamic Options
In addition to a static array, the options of a select field can be a function which receives (formData, context) and returns the option list. This is invaluable when the choices depend on another field, or live in Adaptable's API.
{
name: 'license',
label: 'License',
fieldType: 'select',
options: (formData, context) => {
if (formData.languages?.includes('Rust')) {
return [{ label: 'MIT/Apache (Rust)', value: 'mit-apache' }];
}
return [
{ label: 'MIT', value: 'mit' },
{ label: 'Apache-2.0', value: 'apache-2.0' },
];
},
}Note
The function may also return a Promise for asynchronously-loaded options. The form renders the field with no items until the promise resolves and then re-renders.
Custom Fields
When none of the built-in field types fit your needs, set fieldType: 'custom' and supply a render function. The function receives the field's controlled value, a setValue setter, the entire formData, the Adaptable context, the resolved disabled flag, and (when invalid) the current validation message as error - and returns any React node.
{
name: 'minStars',
label: 'Min Stars',
fieldType: 'custom',
defaultValue: 0,
render: ({ value, setValue, disabled, error }) => (
<>
<input
type="range"
min={0}
max={200000}
step={5000}
value={Number(value) || 0}
disabled={disabled}
onChange={(e) => setValue(Number(e.target.value))}
/>
{error ? <span>{error}</span> : null}
</>
),
}Hint
setValueroutes through the form's normal change pipeline, so per-fieldonValueChange, validation andformIsValidall keep workingerroris the same message the built-in fields show under the control; use it if you want bespoke error placement inside your custom control- A custom field can also be purely read-only - a
renderfunction that ignoresvalueandsetValueand just renders something computed fromformDatais fine
Field-Level Helpers
Each field can declare a few presentational helpers that surface additional information in-place:
| Property | Effect |
|---|---|
placeholder | Empty-state text shown inside text, textarea, number, date, time, datetime and select inputs |
helpText | Inline hint rendered immediately below the input (e.g. "format: YYYY-MM-DD") |
tooltip | Hover tooltip on the field's label |
{
name: 'apiKey',
label: 'API Key',
fieldType: 'text',
placeholder: 'sk-...',
helpText: 'Find your key under Settings → Tokens',
tooltip: 'Used for authenticated requests',
}Hidden & Disabled Fields
Field visibility and edit-state can be data-driven. Both hidden and disabled accept either a static boolean or a function that receives (formData, context) so they can react to other fields' values:
{
name: 'minWatchers',
label: 'Min Watchers',
fieldType: 'number',
hidden: (formData) => !formData.onlyHasWiki, // appear only when checkbox is on
}
{
name: 'reason',
label: 'Reason',
fieldType: 'text',
disabled: (formData) => formData.action === 'delete',
}Hint
Hidden fields are excluded from validation as well as from the rendered DOM, so a required field that's currently hidden will not block form submission
Validation
A field can declare validation rules and the form will surface inline errors and an aggregate formIsValid flag that buttons can gate on.
| Property | Applies to | Effect |
|---|---|---|
required | all editable | Empty value ('', null, undefined, unchecked) is invalid |
min / max | number, slider, date, time, datetime | Numeric or date/time range bounds |
step | number, slider, time, datetime | Stepping interval |
minLength / maxLength | text, textarea | Character-count bounds |
pattern | text | Regular expression that the value must match |
validate(value, formData, ctx) | all editable | Custom validator returning an error message string, or null/undefined if valid |
{
name: 'email',
label: 'Email',
fieldType: 'text',
required: true,
pattern: '^\\S+@\\S+\\.\\S+$',
helpText: 'we will only use this for delivery',
validate: (value, formData) => {
if (formData.notifyByEmail && !value) {
return 'Email is required when notifications are on';
}
return null;
},
}For each field, checks run in this order; the first failure produces the error message and later checks are skipped:
required(empty / unchecked / empty multi-select)- Built-in rules when a value is present:
min/maxfornumberandslider;min/maxfordate,timeanddatetime;minLength/maxLengthfortextandtextarea;patternfortext(see table above for which properties apply to which types). Thestepproperty affects native inputs but is not part of this validation pass. - Custom
validate(value, formData, context)
textOutput fields skip validation entirely.
Note
Per-field errors render below the input (unless you handle them yourself in a custom field via error). The form also produces an aggregate formIsValid: boolean (and a formErrors: Record<string, string> map) which any button declared on the form can read from its context to gate disabled or onClick - e.g. a Submit button that stays disabled until every field is valid.
Reacting to Field Changes
Each Form Field can declare an onValueChange callback that fires only when that specific field changes.
This is designed for ergonomic, control-level reactions.
For instance you can use it to filter the grid as soon as a select changes) without having to inspect the whole form's data via the form-level change callback.
Note
- This is particularly useful for "live" forms where each control should act immediately on change
- e.g. a Custom Toolbar select that filters the grid as soon as the user picks an option, with no Apply button required
{
name: 'language',
label: 'Language',
fieldType: 'select',
options: [...],
onValueChange: (value, context) => {
// `value` is the new value of just this field
// `context.formData` always reflects the latest values (`formData`), including the field that just changed
// this ensures that consumers always see up-to-date values; no extra plumbing needed
},
},Hint
Pair this with the host's form-level change callback (e.g. onToolbarFormChange for Custom Toolbars) when several fields combine to drive a single action
Form Layout
The form's layout property controls how fields are arranged on screen:
'rows'(default) - each field on its own row, label on the left and input on the right; suitable for popups, wizards and full-size forms (Alerts, Export, Data Sets)'inline'- all fields are placed side-by-side on a single horizontal line, with the label rendered immediately before its input; designed for compact contexts such as Dashboard Custom Toolbars where vertical space is at a premium
Hint
Custom Toolbars apply layout: 'inline' automatically - you don't need to set it manually when using toolbarForm
Multiple fields on one row
In the default rows layout, each entry in fields is normally a single AdaptableFormField or a AdaptableFormFieldGroup. You can also supply an array of fields as an entry: every field in that array shares one grid row, with each label/control pair laid out side-by-side. The same array shorthand works inside a group's fields array.
fields: [
[
{ name: 'firstName', label: 'First', fieldType: 'text', required: true },
{ name: 'lastName', label: 'Last', fieldType: 'text', required: true },
],
{ name: 'email', label: 'Email', fieldType: 'text' },
],If every field in an array row is hidden, the whole row is omitted.
Field Groups
Long forms can be split into logical sections by interleaving AdaptableFormFieldGroup entries with regular fields. A group has an optional title and description and contains its own fields array; visually it renders as a labelled section with a divider.
fields: [
{ name: 'name', label: 'Name', fieldType: 'text', required: true },
{
kind: 'group',
title: 'Notification Preferences',
description: 'How would you like us to reach you?',
fields: [
{ name: 'notifyByEmail', label: 'Email', fieldType: 'checkbox' },
{ name: 'notifyBySMS', label: 'SMS', fieldType: 'checkbox' },
],
},
{
kind: 'group',
title: 'Advanced',
hidden: (formData) => !formData.notifyByEmail,
fields: [
{ name: 'replyTo', label: 'Reply-To', fieldType: 'text', placeholder: 'no-reply@…' },
],
},
],Hint
Groups also accept a hidden flag (boolean or function) that lets you collapse an entire section in or out based on form data. A hidden group's fields are excluded from validation just like individual hidden fields.
Note
In the inline layout (e.g. Custom Toolbars) the group's title and description are not rendered - the layout is too compact for sectioning - but the child fields still flow inline alongside the rest.
Form-Level Submit
Set AdaptableForm's onSubmit callback to react when the user presses Enter inside any field input (and the form is valid). This is an ergonomic shortcut that complements - or replaces - an explicit Submit button.
{
fields: [...],
onSubmit: (formData, context) => {
context.adaptableApi.alertApi.showAlert({ Header: 'Submitted', Message: '…' });
},
}Hint
- Pressing
Enterinside atextareainserts a newline rather than submitting (useShift + Enterif you want to override) - this matches user expectations for multi-line input - The hook is not invoked when the form is invalid, so you don't need to re-validate inside the callback
- The first argument is fully typed when you supply a custom
TDatashape (see Strongly-Typed Form Data)
Strongly-Typed Form Data
By default formData is loosely typed as Record<string, any>. When you care about correctness you can declare the shape of your form's data and pass it as the second generic parameter to AdaptableForm:
type MyFormData = {
name: string;
age: number;
notifyByEmail: boolean;
};
const formDef: AdaptableForm<MyFormContext, MyFormData> = {
fields: [...],
onSubmit: (formData, context) => {
// `formData.name` is typed as string, `formData.age` as number, etc.
},
};Inside Custom Toolbars the same TData parameter flows through the host so context.formData, onToolbarFormChange and form-level onSubmit all see the strongly-typed shape:
const customToolbar: CustomToolbar<MyFormData> = {
name: 'MyToolbar',
toolbarForm: {
/* ... */
},
onToolbarFormChange: formData => {
// formData is MyFormData
},
};Resetting Form Data Programmatically
When a form is hosted inside a Custom Toolbar AdapTable owns its data, so a "Clear" button (or any other handler) can roll it back to the declared defaultValues without re-mounting the toolbar:
adaptableApi.dashboardApi.resetCustomToolbarFormData('MyToolbar');
// or - reset every Custom Toolbar that hosts a `toolbarForm`
adaptableApi.dashboardApi.resetCustomToolbarFormData();The reset fires the toolbar's onToolbarFormChange callback once with the freshly-defaulted data, so any external state derived from the form (e.g. a Grid Filter expression) can rebuild itself just like it does when the user types.
Hint
For Alerts, Export and other hosts that own their own form-data lifecycle, simply re-render with a fresh getDefaultAdaptableFormData(formDef) value.
Programmatic helpers
These functions are exported from @adaptabletools/adaptable alongside the form types:
getDefaultAdaptableFormData(formDef)- builds initialformDatafrom each field'sdefaultValue(multi-selects normalise to an array)flattenAdaptableFormFields(formDef)- walksfields, expanding groups and inline field arrays into a single flat list ofAdaptableFormFieldobjects; useful for lookups, metadata, or custom logic that does not want to recurse the treevalidateAdaptableForm(formDef, formData, context)- returns anAdaptableFormErrorsmap using the same rules and hidden-field skipping as the live form (see Validation); use when you host form data outsideAdaptableFormComponentbut want identical behaviourisAdaptableFormFieldGroup(entry)- type guard forkind: 'group'entries when walkingfieldsyourself
import {
flattenAdaptableFormFields,
getDefaultAdaptableFormData,
validateAdaptableForm,
} from '@adaptabletools/adaptable';
const data = getDefaultAdaptableFormData(formDef);
const errors = validateAdaptableForm(formDef, data, context);
const names = flattenAdaptableFormFields(formDef).map(f => f.name);Accessibility
AdaptableFormComponent is wired up to be screen-reader friendly out of the box:
- Each field's
<label>is programmatically associated with its input viahtmlFor/id, so clicking the label focuses the control and screen readers announce the label when the control is focused requiredfields receivearia-required="true"; the visible*marker isaria-hiddento avoid double-announcement- Fields with a current validation error get
aria-invalid="true"and anaria-describedbyreference to the error element, which itself hasrole="alert"so the message is announced live helpTextis also linked viaaria-describedbyso it reads alongside the inputradiofields render as arole="radiogroup"witharia-labelledbypointing at the labelselectfields wrap the underlying combobox in a labelledrole="group"so the relationship to the field's label is preserved through the popup
You don't need to do anything special - just supply meaningful labels and (where relevant) helpText / tooltip strings.
Buttons
Forms can additionally include Buttons to perform actions.
The Buttons are of type AdaptableButton described above.
When a form is rendered through AdaptableFormComponent, each button's handlers receive a context object that includes the usual Adaptable APIs plus the current form snapshot:
context.formData- latest values (same shape asonSubmit's first argument)context.formIsValid-truewhen no field has a validation errorcontext.formErrors- map of fieldnameto error message (absent keys mean valid)
Use these to disable a Submit button until the form is valid, branch onClick on validity, or read errors without re-running validation yourself:
buttons: [
{
label: 'Submit',
tone: 'success',
disabled: (_button, ctx) => !ctx.formIsValid,
onClick: (_button, ctx) => {
if (!ctx.formIsValid) return;
// commit ctx.formData
},
},
],Find Out More
See the UI Guide to the AdapTable Button for detailed information on providing buttons