rilaykit
Forms

Building Forms

How to use the fluent API to build form configurations.

rilay.form(id) (or form.create(rilay, id) with modular packages) returns a form builder wired to your component registry. This page covers adding fields, submit options, and field effects — it assumes a configured rilay instance (Your First Form).

Adding Fields

.add() is polymorphic:

  • .add(field) — one field on its own row
  • .add(field1, field2) — multiple fields on the same row
  • .add([field1, field2]) — equivalent array syntax
import { rilay } from '@/lib/rilay';

const registrationForm = rilay
  .form('registration')
  // Two fields on the same row
  .add(
    { id: 'firstName', type: 'text', props: { label: 'First Name' } },
    { id: 'lastName', type: 'text', props: { label: 'Last Name' } }
  )
  // One field on its own row
  .add({ id: 'email', type: 'email', props: { label: 'Email Address' } });

type must exist in your component registry. addSeparateRows([...]) places each field of an array on its own row.

Field Configuration

interface FieldConfig {
  id?: string; // Auto-generated ('field-1', 'field-2', …) if omitted
  type: string; // Component type from your registry
  props?: Record<string, any>; // Passed to your component renderer
  validation?: FieldValidationConfig; // { validate?, debounceMs? } — see Validation
  conditions?: ConditionalBehavior; // visible/required/disabled/readonly — see Conditions
  effects?: FieldEffect[]; // Reactive side effects (below)
}

When to use .build()

<Form> and workflow steps build the configuration for you. Call .build() only when you need the final serializable FormConfiguration — to save it as JSON, pass it to a third-party tool, or inspect it while debugging.

const formConfig = rilay.form('my-form').add({ id: 'field1', type: 'text' }).build();
console.log(formConfig.allFields);

Submit Options

.setSubmitOptions() sets default submission behavior:

OptionBehavior
forceBypass validation entirely and submit all current values as-is (e.g. "save draft").
skipInvalidRun validation (errors still show in the UI) but exclude invalid fields from the onSubmit data.

When both are set, force wins. Defaults can be overridden at submit time via the useForm() hook (from rilaykit/react):

import { useForm } from 'rilaykit/react';

const { submit } = useForm();
await submit({ force: true });

Field Effects

Effects declare reactive side effects in the field configuration — no useEffect. RilayKit handles subscriptions, cascading, and cleanup. Create them with the onChange() helper from rilaykit:

import { onChange } from 'rilaykit';

const addressForm = rilay
  .form('address')
  .add({
    id: 'country',
    type: 'select',
    props: { label: 'Country', options: countries },
    effects: [
      onChange('country', async (value, { setValue, setProps }) => {
        setValue('city', ''); // reset the child
        setProps('city', { options: await fetchCities(value) }); // reload its options
      }),
    ],
  })
  .add({ id: 'city', type: 'select', props: { label: 'City', options: [] } });

The first argument of onChange() names the field being watched, not the field the effect is declared on — you can attach an effect to a total field that watches price. Effects also run at mount for fields with non-undefined defaultValues, so the example above loads cities immediately when defaultValues={{ country: 'France' }}.

Context API

Each handler receives the new value and a context:

MethodDescription
setValue(fieldId, value)Set another field's value
setProps(fieldId, props)Merge dynamic props into a field
getValues()Snapshot of all current form values
getFieldValue(fieldId)Get a single field's value

The engine has built-in protections — cascade depth limit (10), cycle detection, async abort on rapid changes. Don't handle these manually.

On this page