Field kinds
A field is a plain object. The kind property chooses the behaviour, and everything else configures it. The library never changes a descriptor, so you can freeze one, share it between forms, or generate it from data.
{ name: 'title', displayName: 'Title' } // text (default)
{ name: 'note', kind: 'textarea', allowNewlines: true }
{ name: 'lang', kind: 'select', options: ['en', 'de'] }The ten built-in kinds
kind | Renders | Key options |
|---|---|---|
text (default) | <input> | type, placeholder, autocomplete |
textarea | <textarea> | placeholder, allowNewlines |
select | <dl-select> combobox | options, multiple, placeholder, onOptionsError |
checkbox | checkbox + caption | label |
message | <p> — holds no value | message |
file | file picker + preview | accept |
computed | nothing visible | compute |
cards | clickable option cards | cards |
custom | whatever you render | render |
array | repeating sub-records | of, renderEntry, suggested, … |
You can also add kinds of your own.
Options every kind accepts
interface BaseFieldDescriptor {
name: string; // required, unique within the form
displayName?: TextOrHtml; // label; omit for no label
className?: string; // extra classes on the field wrapper
tab?: Reactive<TabSpec>; // which tab(s) this field belongs to
tooltip?: TextOrHtml | TooltipDescriptor;
isActive?: (ctx) => boolean; // false hides it and drops it from getValues()
onFormChange?: (ctx) => void; // called on every form update
reloadOnChangeOf?: string[]; // dependencies that re-run async work
defaultValue?: Reactive<unknown>;
}name
The key that the value appears under in getValues(). Two fields with the same name raise an error while the form is being built, instead of quietly overwriting each other.
displayName
The label. If you leave it out, the wrapper gets a withoutLabel class instead of an empty <label>. The label is correctly connected to its control through for and id.
tooltip
Either a string, or an object { text, inInput }. With inInput: true, the ? marker sits inside the input instead of next to the label.
{ name: 'url', displayName: 'URL', tooltip: 'Where the data comes from.' }
{ name: 'key', displayName: 'Key', tooltip: { text: 'Secret.', inInput: true } }You can also change a tooltip while the form is open — see setTooltipError and the related methods.
Reactive<T>: literal or function
Most options accept either a fixed value or a function of the form context. This is what makes a descriptor live instead of static:
type Reactive<T> = T | ((ctx: FieldContext) => T);// literal
{ name: 'year', defaultValue: '2026' }
// function of current form data
{ name: 'year', defaultValue: () => String(new Date().getFullYear()) }
// placeholder that follows another field
{
name: 'locator',
placeholder: ({ data }) => `enter a ${String(data['kind'] ?? 'page')}`,
}The context object
Every callback receives a single context object. There are never several positional arguments:
interface FieldContext {
data: FormValues; // this form's current values
form: DeclarativeForm; // the owning form
field: FieldDescriptor; // the descriptor being evaluated
parentData: FormValues | undefined; // enclosing form, when nested
stackData: readonly FormValues[]; // every open dialog, outermost first
isEditingArrayEntry: boolean; // true in an "edit entry" dialog
}Two callbacks get more than that:
onFormChangealso getstrigger: the field whose input started the update, orundefined.- The
renderfunction of acustomfield also getselement,requestUpdate()andsetValue().
Take only the properties you need:
{
name: 'token',
isActive: ({ data }) => data['source'] === 'GitLab',
}Values and types
getValues() returns a Record<string, unknown>. This is what each kind puts into it:
| Kind | Value type | Empty value |
|---|---|---|
text, textarea | string | '' |
select | string | '' |
select with multiple | string[] | [] |
checkbox | boolean | false |
file | string (a URL) | '' |
cards | string | '' |
computed | whatever compute returns | '' before first run |
custom | whatever you setValue() | '' |
array | FormValues[] | [] |
message | (absent from getValues()) | — |
A message field has no value
A message field only displays text. It does not appear in getValues() at all — not even as an empty string. This is different from v1, which returned '' for it.