Skip to content

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.

ts
{ name: 'title', displayName: 'Title' }                        // text (default)
{ name: 'note', kind: 'textarea', allowNewlines: true }
{ name: 'lang', kind: 'select', options: ['en', 'de'] }

The ten built-in kinds

kindRendersKey options
text (default)<input>type, placeholder, autocomplete
textarea<textarea>placeholder, allowNewlines
select<dl-select> comboboxoptions, multiple, placeholder, onOptionsError
checkboxcheckbox + captionlabel
message<p> — holds no valuemessage
filefile picker + previewaccept
computednothing visiblecompute
cardsclickable option cardscards
customwhatever you renderrender
arrayrepeating sub-recordsof, renderEntry, suggested, …

You can also add kinds of your own.

Options every kind accepts

ts
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.

ts
{ 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:

ts
type Reactive<T> = T | ((ctx: FieldContext) => T);
ts
// 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:

ts
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:

  • onFormChange also gets trigger: the field whose input started the update, or undefined.
  • The render function of a custom field also gets element, requestUpdate() and setValue().

Take only the properties you need:

ts
{
  name: 'token',
  isActive: ({ data }) => data['source'] === 'GitLab',
}

Values and types

getValues() returns a Record<string, unknown>. This is what each kind puts into it:

KindValue typeEmpty value
text, textareastring''
selectstring''
select with multiplestring[][]
checkboxbooleanfalse
filestring (a URL)''
cardsstring''
computedwhatever compute returns'' before first run
customwhatever you setValue()''
arrayFormValues[][]
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.

Released under the MIT License.