Introduction

Core concepts

Understand how Nestled Forms works — the dual API, FormFieldClass factory, form context, and the patterns that make the library powerful.


The dual API

Nestled Forms provides two ways to build forms: declarative and imperative. You can use either approach exclusively, or combine them in a single form.

Declarative

Pass an array of field definitions to the Form component's fields prop. The form renders all fields automatically in order.

import { Form, FormFieldClass } from '@nestledjs/forms'

const fields = [
  FormFieldClass.text('firstName', { label: 'First Name', required: true }),
  FormFieldClass.text('lastName', { label: 'Last Name', required: true }),
  FormFieldClass.email('email', { label: 'Email', required: true }),
]

<Form id="contact" fields={fields} submit={(values) => save(values)}>
  <button type="submit">Submit</button>
</Form>

When to use: Standard forms where you want concise, data-driven field definitions. Great for forms generated from configuration or when field order matches render order.

Imperative

Use the RenderFormField component to place individual fields anywhere inside the form. No fields prop is needed on the Form component.

import { Form, RenderFormField, FormFieldClass } from '@nestledjs/forms'
;<Form id="contact" submit={(values) => save(values)}>
  <div className="grid grid-cols-2 gap-4">
    <RenderFormField
      field={FormFieldClass.text('firstName', { label: 'First Name' })}
    />
    <RenderFormField
      field={FormFieldClass.text('lastName', { label: 'Last Name' })}
    />
  </div>
  <RenderFormField field={FormFieldClass.email('email', { label: 'Email' })} />
  <button type="submit">Submit</button>
</Form>

When to use: Complex layouts, forms with custom content between fields, or when you need programmatic control over field placement.

Mixed

Combine both: use the fields array for the bulk of the form, and add additional RenderFormField components for extra fields that need special positioning.

const baseFields = [
  FormFieldClass.text('name', { label: 'Name' }),
  FormFieldClass.email('email', { label: 'Email' }),
]

<Form id="mixed" fields={baseFields} submit={save}>
  <h3>Additional Information</h3>
  <RenderFormField field={FormFieldClass.textArea('notes', { label: 'Notes' })} />
  <button type="submit">Submit</button>
</Form>

FormFieldClass

FormFieldClass is a factory class with static methods for creating field definitions. Each method corresponds to a field type and returns a typed field configuration object.

Factory methods

Every factory method follows the same signature:

FormFieldClass.fieldType(key: string, options?: FieldOptions)
  • key — A unique string identifier for the field. This becomes the property name in the form values object.
  • options — An optional configuration object. Each field type has its own specific options, plus shared options that all fields support.

Example

const field = FormFieldClass.text('username', {
  label: 'Username',
  required: true,
  placeholder: 'Enter your username',
  validate: (value) => value.length >= 3 || 'Must be at least 3 characters',
})

The returned object contains the field type, key, and all options. You never need to construct field objects manually.


Shared field options

Every field type inherits these options:

Display

OptionTypeDescription
labelstringLabel text displayed above the field
placeholderstringPlaceholder text inside the field
helpTextstringHelp text displayed below the field
hiddenbooleanWhether the field is hidden

State

OptionTypeDescription
requiredbooleanWhether the field is required
disabledbooleanWhether the field is disabled
readOnlybooleanWhether the field is read-only
readOnlyStyle'value' | 'disabled'How read-only fields appear
defaultValueanyDefault value for the field

Conditional logic

OptionTypeDescription
showWhen(formValues) => booleanShow/hide based on other field values
requiredWhen(formValues) => booleanMake required based on conditions
disabledWhen(formValues) => booleanDisable based on conditions
validateWhen(formValues) => booleanOnly validate when condition is met

Validation

OptionTypeDescription
validate(value) => true | string | PromiseValidation function
schemaZodTypeAnyZod validation schema
validateWithForm(value, formValues) => true | stringCross-field validation
validationDependenciesstring[]Fields that trigger re-validation
validationGroupstringGroup for multi-step validation
errorMessagesobjectCustom error messages

Layout

OptionTypeDescription
wrapperClassNamestringCSS class for the field wrapper
layout'horizontal' | 'vertical'Label/input arrangement
customWrapper(children) => JSX.ElementCustom wrapper component

Transform

OptionTypeDescription
submitTransform(value) => unknownTransform value before form submission

Form component

The Form component is the top-level wrapper for all fields. It manages form state, validation, and submission.

Props

interface FormProps<T> {
  id: string // Unique form identifier
  fields?: FormField[] // Declarative field definitions
  submit: (values: T) => void // Submit handler
  initialValues?: Partial<T> // Pre-fill form values
  readOnly?: boolean // Make all fields read-only
  theme?: FormTheme // Custom theme
  validateOnBlur?: boolean // Validate fields on blur
  validateOnChange?: boolean // Validate fields on change
  children?: React.ReactNode // Child elements (buttons, imperative fields)
}

Example with all features

<Form<InvoiceFormValues>
  id="invoice-form"
  fields={invoiceFields}
  initialValues={{ currency: 'USD', status: 'DRAFT' }}
  submit={handleSubmit}
  readOnly={isViewMode}
  theme={customTheme}
  validateOnBlur
>
  <div className="flex gap-4">
    <button type="submit">Save Invoice</button>
    <button type="button" onClick={onCancel}>
      Cancel
    </button>
  </div>
</Form>

Form context

Inside a Form, any child component can access form state through the useFormContext hook.

import { useFormContext } from '@nestledjs/forms'

function SubmitButton() {
  const { formValues, errors, isSubmitting } = useFormContext()

  return (
    <button
      type="submit"
      disabled={isSubmitting || Object.keys(errors).length > 0}
    >
      {isSubmitting ? 'Saving...' : 'Submit'}
    </button>
  )
}

The context provides:

PropertyTypeDescription
formValuesobjectCurrent values of all fields
errorsobjectCurrent validation errors
isSubmittingbooleanWhether the form is submitting
setValue(key, value) => voidProgrammatically set a field value
setError(key, error) => voidProgrammatically set a field error
reset() => voidReset form to initial values

Form config context

Access form configuration through useFormConfig:

import { useFormConfig } from '@nestledjs/forms'

function CustomField() {
  const { readOnly, validateOnBlur } = useFormConfig()
  // ...
}

Theme context

Access the current theme through useFormTheme:

import { useFormTheme } from '@nestledjs/forms'

function CustomField() {
  const theme = useFormTheme()
  return <input className={theme.input} />
}

Field types at a glance

CategoryField types
Text inputtext, textArea, email, password, url, phone
Numericnumber, currency
Selectionselect, multiSelect, enumSelect, radio, checkboxGroup
SearchsearchSelect, searchSelectApollo, searchSelectMulti, searchSelectMultiApollo
Booleancheckbox, switch, customCheckbox
Date & timedatePicker, dateTimePicker, timePicker
Rich contentmarkdownEditor, content, custom, button

Each category has a dedicated documentation page with detailed options, examples, and usage patterns.

Previous
Installation