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:

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:

PropertyTypeDescriptionDefault
buttonsAdaptableButton<T>[]Buttons to include in the Form
descriptionstringAdditional information to appear in the Form
fields(AdaptableFormField|AdaptableFormField[] |AdaptableFormFieldGroup)[]Collection of Dynamic Fields and Field Groups to display.
layoutAdaptableFormLayoutHow the form's fields are arranged on screen.'rows'
onSubmit(formData: TData, context: T) => voidOptional form-level submit hook.
titlestringTitle 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 Form
  • description - 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:

PropertyTypeDescriptionDefault
clearToDefaultbooleanFor single select fields only. When the user clears the combobox, whether to restore instead of leaving the field empty.
defaultValuestring | 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.
disabledboolean | ((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.
fieldTypeAdaptableFormFieldTypeField Type - dictates which UI control is rendered
helpTextstringInline help text rendered immediately below the field's input. Useful for short explanations such as "format: YYYY-MM-DD" or "min 3 chars".
hiddenboolean | ((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.
labelstringLabel to display in the Field
maxnumber | stringMaximum allowed value. For number and slider: the highest accepted number. For date, time and datetime: the latest allowed value in ISO format.
maxLengthnumberMaximum length for text and textarea fields.
minnumber | stringMinimum 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).
minLengthnumberMinimum length for text and textarea fields.
multibooleanFor 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
namestringName of the Field
onValueChange(value: any, context:BaseContext) => voidOptional callback invoked whenever value of this field changes
optionsAdaptableFormFieldOption[] | ((formData:AdaptableFormData, context:BaseContext) =>AdaptableFormFieldOption[] | Promise<AdaptableFormFieldOption[]>)Items to populate the select and radio fieldTypes.
patternstringRegular expression that the value of a text field must match.
placeholderstringPlaceholder 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.ReactNodeFor 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.
requiredbooleanIf true the field is treated as required. An empty value ('', null, undefined, or unchecked checkbox) will produce a validation error.
rowsnumberFor textarea fields: the visible number of text rows.3
stepnumberStepping interval for number, slider, time and datetime fields.
tooltipstringTooltip shown when hovering the field's label.
validate(value: any, formData:AdaptableFormData, context:BaseContext) => string | null | undefinedCustom 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:

fieldTypeUI control
textSingle-line text input
textareaMulti-line text input - configurable rows (defaults to 3)
numberNumeric input
sliderRange slider with min/max/step plus an inline value read-out - see Slider
dateNative HTML5 date picker
timeTime-of-day picker (HH:mm)
datetimeCombined date + time picker
colorNative HTML5 colour picker (value is a #RRGGBB string)
selectCombobox populated from options (single or multi - see Multi-Select)
radioMutually-exclusive group of radio buttons populated from options - see Radio Group
checkboxCheckbox
textOutputRead-only inline label - displays the field's current value as plain text (useful for static guidance or computed labels alongside other controls)
customRenders 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 an HH:mm (or HH:mm:ss when step is sub-minute) string
  • datetime - combined date + time, value is YYYY-MM-DDTHH:mm
  • color - HTML5 colour picker, value is a #RRGGBB string
{ 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 sync

Dynamic 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

  • setValue routes through the form's normal change pipeline, so per-field onValueChange, validation and formIsValid all keep working
  • error is 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 render function that ignores value and setValue and just renders something computed from formData is fine

Field-Level Helpers

Each field can declare a few presentational helpers that surface additional information in-place:

PropertyEffect
placeholderEmpty-state text shown inside text, textarea, number, date, time, datetime and select inputs
helpTextInline hint rendered immediately below the input (e.g. "format: YYYY-MM-DD")
tooltipHover 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.

PropertyApplies toEffect
requiredall editableEmpty value ('', null, undefined, unchecked) is invalid
min / maxnumber, slider, date, time, datetimeNumeric or date/time range bounds
stepnumber, slider, time, datetimeStepping interval
minLength / maxLengthtext, textareaCharacter-count bounds
patterntextRegular expression that the value must match
validate(value, formData, ctx)all editableCustom 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:

  1. required (empty / unchecked / empty multi-select)
  2. Built-in rules when a value is present: min / max for number and slider; min / max for date, time and datetime; minLength / maxLength for text and textarea; pattern for text (see table above for which properties apply to which types). The step property affects native inputs but is not part of this validation pass.
  3. 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 Enter inside a textarea inserts a newline rather than submitting (use Shift + Enter if 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 TData shape (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 initial formData from each field's defaultValue (multi-selects normalise to an array)
  • flattenAdaptableFormFields(formDef) - walks fields, expanding groups and inline field arrays into a single flat list of AdaptableFormField objects; useful for lookups, metadata, or custom logic that does not want to recurse the tree
  • validateAdaptableForm(formDef, formData, context) - returns an AdaptableFormErrors map using the same rules and hidden-field skipping as the live form (see Validation); use when you host form data outside AdaptableFormComponent but want identical behaviour
  • isAdaptableFormFieldGroup(entry) - type guard for kind: 'group' entries when walking fields yourself
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 via htmlFor / id, so clicking the label focuses the control and screen readers announce the label when the control is focused
  • required fields receive aria-required="true"; the visible * marker is aria-hidden to avoid double-announcement
  • Fields with a current validation error get aria-invalid="true" and an aria-describedby reference to the error element, which itself has role="alert" so the message is announced live
  • helpText is also linked via aria-describedby so it reads alongside the input
  • radio fields render as a role="radiogroup" with aria-labelledby pointing at the label
  • select fields wrap the underlying combobox in a labelled role="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 as onSubmit's first argument)
  • context.formIsValid - true when no field has a validation error
  • context.formErrors - map of field name to 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