Skip to content

TypeScript reference

This page lists every type exported from @samline/forms, what it represents, and where it shows up. Each type links to the method(s) that consume or produce it.

All types are exported from the package root:

import type {
AppendContentOptions,
AutoSubmitOptions,
FieldFormatConfig,
FieldFormatConfigMap,
FieldValidationContext,
FieldValidationRules,
FieldValidator,
FormatType,
FormController,
FormControllerOptions,
FormCleanup,
FormDataPrimitive,
FormErrors,
FormFieldElement,
FormFieldValue,
FormFieldWatcher,
FormsApi,
FormsAvailable,
FormStateListener,
FormStateSnapshot,
FormSubmitHandler,
FormTarget,
FormValues,
NewFormInput,
RuleConfig,
SerializedFormResult,
SerializedFormValue,
ValidationResult,
ValidationSchema,
ValueValidationRules,
VisualAttributes
} from '@samline/forms'

The full controller surface returned by form().

interface FormController {
readonly element: HTMLFormElement | null
readonly f: HTMLFormElement | null
readonly options: FormControllerOptions
onSubmit: (callback: FormSubmitHandler, preventDefault?: boolean) => FormController
addCleanup: (cleanup: FormCleanup) => () => void
watch: (field: string, callback: FormFieldWatcher) => FormController
observe: (field: string, callback: FormFieldWatcher) => () => void
unwatch: (field?: string, callback?: FormFieldWatcher) => FormController
subscribe: (listener: FormStateListener) => () => void
prefill: (fieldName?: string) => FormController
append: (options: AppendContentOptions) => HTMLElement | null
setErrors: (fields: string[] | FormErrors) => FormController
clearErrors: (fields?: string[]) => FormController
setValue: (name: string, value: unknown) => FormController
validate: (fields?: string[]) => ValidationResult
revalidate: (fields?: string[]) => ValidationResult
format: (config: FieldFormatConfig) => FormController
formatAll: (config: FieldFormatConfig) => FormController
reset: () => FormController
autoSubmit: (options?: boolean | AutoSubmitOptions) => FormController
disableAutoSubmit: () => FormController
getValue: (name: string) => FormFieldValue
getField: (name: string) => FormFieldElement | FormFieldElement[] | null
getData: () => SerializedFormResult
getState: () => FormStateSnapshot
destroy: () => void
}

The second argument to form(). See Configuration for the full reference.

interface FormControllerOptions {
attributes?: Partial<VisualAttributes>
autoValidate?: boolean
autoSubmit?: boolean | AutoSubmitOptions
clearManualErrorsOnChange?: boolean
clearErrorsOnSubmit?: boolean
validators?: ValidationSchema
formats?: FieldFormatConfigMap
}

What form() accepts as its first argument.

type FormTarget =
| string // element id
| HTMLFormElement // direct element
| { current: HTMLFormElement | null } // ref-like
| null
| undefined

A string is treated as a document.getElementById(id) lookup; only HTMLFormElement matches count. A ref-like value lets you pass a Vue/React-style ref object.

The set of DOM field types the controller manages.

type FormFieldElement =
| HTMLInputElement
| HTMLSelectElement
| HTMLTextAreaElement

Returned by getField.

The normalized value of a field.

type FormFieldValue = string | string[] | File[] | undefined
  • string — for text inputs, textareas, single selects, and single checked checkboxes / radios.
  • string[] — for checkbox groups, multiple selects, and repeated name="field[]" controls. Radio groups remain scalar.
  • File[] — for <input type="file"> (may be empty).
  • undefined — when no field with that name exists.

Returned by getValue.

The aggregated values map produced by the controller and the helpers.

type FormValues = Record<string, FormFieldValue>

Useful when you build custom validators that need cross-field logic beyond exact equality (sameAs handles equality directly):

form('booking-form', {
validators: {
end_date: {
dependsOn: 'start_date',
validate: ({ value, values }) =>
typeof value === 'string' &&
typeof values.start_date === 'string' &&
value >= values.start_date
? null
: 'End date must not precede start date.'
}
}
})

The controller infers reactive dependencies from sameAs; use dependsOn for arbitrary reads inside validate. Here a change to start_date revalidates end_date once validation is active.

A map from field name to an array of error messages. Each field can have multiple errors (one per failed rule or per failing custom validator).

type FormErrors = Record<string, string[]>

Drives the css-error attribute on fields and is returned inside FormStateSnapshot and ValidationResult.

The shape returned by getState(). Built fresh on every call — it does not retain references to controller internals.

interface FormStateSnapshot {
values: FormValues
errors: FormErrors
filledFields: string[]
isValid: boolean
isValidated: boolean
autoSubmit: boolean
isSubmitting: boolean
submitCount: number
}
Field Meaning
values Current values for every tracked field.
errors Merged validation and manual errors.
filledFields Names of fields that have a non-empty value.
isValid true when errors has no entries.
isValidated true once validate has run at least once.
autoSubmit true while auto-submit is enabled.
isSubmitting true while at least one valid submission has pending async handlers. Overlapping submissions remain tracked until all their handlers settle.
submitCount Number of submit attempts (valid or invalid).

The callback passed to subscribe.

type FormStateListener = (state: FormStateSnapshot) => void

Receives the current snapshot immediately, then at controller notification points such as handled input, manual-error changes, reset, submit attempts, auto-submit toggles, and observed DOM mutations. A direct validate() call does not independently notify it.

The callback passed to onSubmit.

type FormSubmitHandler = (
form: HTMLFormElement,
data: Record<string, SerializedFormValue>,
formData: FormData,
state: FormStateSnapshot
) => void | Promise<void>

Only invoked when the form is valid. data and formData are produced fresh on each invocation. Promise-returning handlers contribute to isSubmitting; fulfillment and rejection both settle tracking.

The callback accepted by addCleanup().

type FormCleanup = () => void

Cleanups run in reverse registration order during destroy(). addCleanup() returns an idempotent function that unregisters the callback without running it.

The callback passed to watch and observe.

type FormFieldWatcher = (
value: FormFieldValue,
field: FormFieldElement | FormFieldElement[] | null,
form: HTMLFormElement,
state: FormStateSnapshot
) => void
  • value — the current value of the field that just changed.
  • field — the DOM element(s) backing the field. Single matching field → HTMLInputElement / HTMLSelectElement / HTMLTextAreaElement; repeated fields → array; no match → null.
  • form — the bound HTMLFormElement.
  • state — a snapshot of the whole controller state.

observe and watch fire once immediately with the current value, then on every matching input event. observe returns an unsubscribe function; watch returns the controller.

The shape returned by getData() and parseFormData().

interface SerializedFormResult {
data: Record<string, SerializedFormValue>
formData: FormData
}

data is a plain object mirror of the FormData. Repeated names become arrays. Empty file entries are filtered out.

The value shape inside SerializedFormResult.data.

type SerializedFormValue = FormDataPrimitive | FormDataPrimitive[]
type FormDataPrimitive = FormDataEntryValue // string | File

The shape returned by validate, revalidate, and validateValues.

interface ValidationResult {
isValid: boolean
errors: FormErrors
}

errors is always a fresh object — safe to mutate.

The shape of options.validators.

type ValidationSchema = Record<string, FieldValidationRules>

The rule set for a single field.

interface FieldValidationRules extends ValueValidationRules {
sameAs?: RuleConfig<string>
dependsOn?: string | string[]
each?: ValueValidationRules
}

Value rules run in the order: required → minLength → maxLength → pattern → numeric/range checks → validate. sameAs then runs at field level, and each applies its value rules to every member. All are optional; an empty rules object contributes nothing. dependsOn only declares reactive sources and does not produce an error.

sameAs names another exact field key. It compares non-empty strings exactly and arrays by ordered contents; file entries compare by File object identity. In a controller, changing the referenced field automatically revalidates the field that declares sameAs. Reciprocal declarations are cycle-safe, but usually duplicate the same error on both controls; prefer declaring the rule only on the confirmation field.

The exported subset used for whole values and inside FieldValidationRules.each.

interface ValueValidationRules {
required?: RuleConfig<boolean>
minLength?: RuleConfig<number>
maxLength?: RuleConfig<number>
pattern?: RuleConfig<RegExp>
numeric?: RuleConfig<boolean>
min?: RuleConfig<number>
max?: RuleConfig<number>
validate?: FieldValidator | FieldValidator[]
}

numeric, min, and max skip empty values. Non-empty values must be finite signed decimal strings; bounds are inclusive. sameAs, dependsOn, and nested each are field-level concerns and are intentionally absent.

Lets a rule be configured with a plain value or a { value, message } object.

type RuleConfig<T> = T | { value: T; message?: string }
form('signup-form', {
validators: {
password: {
minLength: { value: 8, message: 'Use at least 8 characters.' }
},
terms: {
required: { value: true, message: 'Please accept the terms.' }
}
}
})

When the plain form is used, default messages are produced automatically.

The argument passed to a custom validator.

interface FieldValidationContext {
field: string
value: FormFieldValue
values: FormValues
element?: FormFieldElement
index?: number
}

element and zero-based index are present while a validator runs through each; they are omitted for group-level validation. Use dependsOn to declare fields read by cross-field validators:

form('checkout-form', {
validators: {
card: {
validate: ({ value, field }) => {
if (typeof value !== 'string') return `${field} is required.`
return /^\d{16}$/.test(value.replace(/\s+/g, ''))
? null
: 'Card number must be 16 digits.'
}
}
}
})

The custom validator signature.

type FieldValidator = (
context: FieldValidationContext
) => string | undefined | null | false | true
Return Meaning
string (non-empty) Push as the error message.
undefined, null, true Pass.
false Push the generic error message "Validation failed.".

Multiple validators can be chained by passing an array as validate:

form('order-form', {
validators: {
quantity: {
validate: [
({ value }) => (typeof value === 'string' && Number(value) > 0 ? null : 'Must be greater than zero.'),
({ value }) => (typeof value === 'string' && Number.isInteger(Number(value)) ? null : 'Must be an integer.')
]
}
}
})

The argument to autoSubmit() and the autoSubmit option.

interface AutoSubmitOptions {
debounce?: number
}

debounce is in milliseconds.

The argument to append().

interface AppendContentOptions {
tag: keyof HTMLElementTagNameMap
content: string
class?: string
atStart?: boolean
}
Field Meaning
tag The HTML tag to create.
content innerHTML content for the new node.
class Optional class name. If a node with the same first class already exists inside the form, it is removed before the new one is inserted.
atStart When true, inserts at the start of the form. Defaults to false (append at the end).

The shape of options.attributes.

interface VisualAttributes {
filled: string
error: string
}

Partial<VisualAttributes> is accepted, so you can override only one of them. See CSS styling.

FieldFormatConfig and FieldFormatConfigMap

Section titled “FieldFormatConfig and FieldFormatConfigMap”

The argument to format() and formatAll().

interface FieldFormatConfig {
type: FormatType
field: string | string[]
displayField?: string
options?: Record<string, unknown>
}
type FormatType =
| 'general'
| 'phone'
| 'numeral'
| 'date'
| 'time'
| 'creditCard'
| 'creditCardType'
type FieldFormatConfigMap = Record<string, FieldFormatConfig>

The field name is the canonical name of the formatted pair. A custom displayField is valid only for one string field; passing it with a field array logs an error and leaves the form unchanged. Array configurations derive ${fieldName}_displayed independently for every field. Every map value includes its own field because the map key is only an identifier.

The shape of the browser singleton exported from the vanilla entrypoint. The IIFE bundle exposes the same shape as window.Forms.

interface FormsApi {
form: (target: FormTarget, options?: FormControllerOptions) => FormController
newForm: (input: NewFormInput) => FormController | undefined
destroyForm: (id: string) => void
available: FormsAvailable
}

form is the same reference exported from @samline/forms. newForm and destroyForm manage the module-level available registry keyed by form id. Spreading browser copies references but does not create an independent registry: the methods close over the same available object. Use form() plus your own Map when isolation is required.

The argument to FormsApi.newForm.

interface NewFormInput {
id: string
options?: FormControllerOptions
}

A missing id logs Form ID is required to console.error and newForm returns undefined without touching the registry.

The shape of FormsApi.available.

interface FormsAvailable {
[id: string]: FormController
}

Use it to inspect or iterate over every live controller in the registry. Each entry is the FormController returned by newForm for that id.