Forms

Every control submits like a native input.

Give a control a name and it submits through FormData, the shape a plain form, Inertia <Form>, React Router actions, and server actions all read. No onChange wiring, no hidden-input glue.

Native submission for DatePicker, DateRangePicker, TagInput, FileUpload, Dropdown, and Rating, plus parseDateOnly, needs @dev-dga/react 1.4.0 or later.

What each control submits

The rules match native inputs: a disabled control submits nothing, required blocks submission while the control is empty, and a form reset restores the initial value.

ControlSubmitsNotes
TextInput, Textarea, SearchBoxThe textNative input; every input prop passes through.
NumberInputPlain decimal string, such as "1234.5"No digit grouping, always Latin digits. Empty submits "".
SelectThe selected valueEmpty submits "".
DropdownThe selected valueWith multiple, one entry per value under the same name. Empty single submits "".
Checkboxvalue, or "on"Unchecked submits nothing. Give a group one name and read it with getAll.
RadioGroupThe selected valueNo selection submits nothing.
Switch"on"Off submits nothing.
SliderThe valueA range submits name[] once per thumb; Radix appends the brackets.
RatingNumber string, such as "4" or "3.5"Unrated submits "0".
TagInputOne entry per tagNo tags submits nothing. The uncommitted draft text never submits.
InputOTPThe entered codeNative input under the slots.
DatePickerGregorian YYYY-MM-DDEven while Hijri is displayed. Empty submits "".
DateRangePickerGregorian YYYY-MM-DD under startName and endNameEach end submits separately.
FileUploadEvery listed file (multipart)The native input mirrors files, dropped ones included. A reset leaves files to you.
ContentSwitcher, ToggleNothingView switchers, not form controls. For a form choice, use RadioGroup or Checkbox.

Multiple values

TagInput, Dropdown with multiple, and Checkbox groups submit one FormData entry per value under the exact name you pass, like a native <select multiple>. Read them with formData.getAll(name). PHP and Laravel only build an array from names ending in [], so pass name="tags[]" there.

// Next.js server action or React Router actionformData.getAll('skills'); // ["react", "laravel"]// Laravel reads arrays only from names ending in []<TagInput label="Skills" name="skills[]" /><Dropdown multiple label="Cities" name="cities[]">…</Dropdown>// Laravel: $request->validate(['skills' => ['array'], 'skills.*' => ['string']]);

Plain forms

Wrap the controls in a <form> and read FormData in the submit handler. This is the contract every integration below builds on.

<form  onSubmit={(event) => {    event.preventDefault();    const data = new FormData(event.currentTarget);    data.get('fullName'); // "Noura"    data.get('visitDate'); // "2026-10-20"    data.getAll('skills'); // ["react", "laravel"]  }}>  <TextInput label="Full name" name="fullName" required />  <DatePicker label="Visit date" name="visitDate" required />  <TagInput label="Skills" name="skills" />  <Button type="submit">Submit</Button></form>

Inertia <Form>

Inertia builds its payload from new FormData(form), so named controls need no extra wiring. Its errors map straight onto each control’s error and errorMessage props.

import { Form } from '@inertiajs/react';import { Button, DatePicker, Select, SelectItem, TextInput, parseDateOnly } from '@dev-dga/react';export default function EditVisit({ visit }: { visit: Visit }) {  return (    <Form action={`/visits/${visit.id}`} method="put">      {({ errors, processing }) => (        <>          <TextInput            label="Name"            name="name"            defaultValue={visit.name}            error={!!errors.name}            errorMessage={errors.name}          />          <Select label="City" name="city" defaultValue={visit.city} error={!!errors.city} errorMessage={errors.city}>            <SelectItem value="riyadh">Riyadh</SelectItem>            <SelectItem value="jeddah">Jeddah</SelectItem>          </Select>          <DatePicker            label="Visit date"            name="date"            defaultValue={visit.date ? parseDateOnly(visit.date) : null}            error={!!errors.date}            errorMessage={errors.date}          />          <Button type="submit" disabled={processing}>            Save          </Button>        </>      )}    </Form>  );}

A full reset() fires a reset event that the controls listen for. A per-field reset, such as resetOnSuccess={["name"]}, fires no event, so controls that keep their own state (date pickers, TagInput, Dropdown, Rating, NumberInput, InputOTP) keep their current value. Reset the whole form, or control those fields yourself.

React Router actions

React Router’s <Form> posts the same FormData to the route action. Read it with request.formData() and return validation errors from the action.

import { Form, useActionData, type ActionFunctionArgs } from 'react-router';import { Button, DatePicker } from '@dev-dga/react';export async function action({ request }: ActionFunctionArgs) {  const data = await request.formData();  const date = data.get('date'); // "2026-10-20", or "" when empty  if (!date) return { errors: { date: 'Pick a visit date.' } };  // save…  return { errors: {} };}export default function NewVisit() {  const result = useActionData<typeof action>();  return (    <Form method="post">      <DatePicker        label="Visit date"        name="date"        error={!!result?.errors.date}        errorMessage={result?.errors.date}      />      <Button type="submit">Save</Button>    </Form>  );}

Server actions

Pass a server action to the form’s action prop. useActionState returns the action’s result, so the server’s field errors flow back into the controls.

// app/visits/actions.ts'use server';type State = { errors: { date?: string } };export async function saveVisit(_prev: State, formData: FormData): Promise<State> {  const date = formData.get('date');  if (typeof date !== 'string' || date === '') return { errors: { date: 'Pick a visit date.' } };  // save…  return { errors: {} };}// app/visits/visit-form.tsx'use client';import { useActionState } from 'react';import { Button, DatePicker } from '@dev-dga/react';import { saveVisit } from './actions';export function VisitForm() {  const [state, formAction, pending] = useActionState(saveVisit, { errors: {} });  return (    <form action={formAction}>      <DatePicker        label="Visit date"        name="date"        error={!!state.errors.date}        errorMessage={state.errors.date}      />      <Button type="submit" disabled={pending}>        Save      </Button>    </form>  );}

Server validation errors

Every field takes error (the invalid state) and errorMessage (the text below the field). Together they set aria-invalid and point aria-describedby at the message, so screen readers announce the server’s message with the field.

<TextInput  label="National ID"  name="nationalId"  error={!!errors.nationalId}  errorMessage={errors.nationalId}/><Dropdown label="City" name="city" error={!!errors.city} errorMessage={errors.city}>  …</Dropdown>

Set error and errorMessage together. errorMessage without error stays hidden, and error alone marks the field without telling the user why.

Dates from the server

Servers send dates as YYYY-MM-DD strings. Convert them with parseDateOnly, which builds a local-midnight Date and throws a RangeError on any other shape. new Date("2026-10-20") parses as UTC, so anywhere west of UTC it shows October 19.

import { DatePicker, parseDateOnly } from '@dev-dga/react';// visit.date arrives as "2026-10-20" (or null)const visitDate = visit.date ? parseDateOnly(visit.date) : null;<DatePicker label="Visit date" name="visitDate" defaultValue={visitDate} />;// Avoid: new Date(visit.date) parses as UTC and shows October 19 west of UTC.