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:
| Option | Behavior |
|---|---|
force | Bypass validation entirely and submit all current values as-is (e.g. "save draft"). |
skipInvalid | Run 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:
| Method | Description |
|---|---|
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.