# Forms reference

Detailed field, upload and transaction behavior for application authors.

## Standalone forms

```go
definition, err := forms.New([]forms.Field{
    forms.Text(forms.Options{Name: "email", Label: "Email", Widget: "email", Required: true}, forms.Email),
    forms.Integer(forms.Options{Name: "age", Label: "Age", Required: true}, forms.Range[int64](18, 120)),
    forms.Boolean(forms.Options{Name: "accepted", Label: "Accept terms"}, forms.Accepted),
    forms.Multiple(forms.Options{Name: "topics", Label: "Topics", Choices: []forms.Choice{
        {Value: "go", Label: "Go"}, {Value: "sql", Label: "SQL"},
    }}),
})
// Check configuration errors before serving requests.
posted, err := forms.Parse(r)
// Handle parse errors and apply request body limits.
bound := definition.Bind(posted.Values)
if bound.Valid() {
    age, ok := forms.Value[int64](bound, "age")
    // Use age after checking ok; values have already passed validators.
}
```

`Initial()` supplies initial values without claiming the form was submitted or
valid. `Bind` snapshots input and ignores undeclared names. Single-value fields
reject repeated values. Multiple-choice fields reject duplicate or unavailable
choices. Invalid UTF-8, numeric overflow, non-finite numbers, invalid dates and
malformed timestamps are rejected. Strings preserve whitespace; Required checks
trimmed emptiness. Optional numeric/boolean fields become zero/false; optional
dates/timestamps become zero time. `Date` accepts YYYY-MM-DD; `Timestamp` requires
an RFC3339 offset and returns UTC. There is no implicit locale conversion.

Built-ins are `Text`, `Integer` (int64), `Number` (float64), `Boolean`, `Date`,
`Timestamp`, `Select`, and `Multiple`. `Typed[T]` supports explicit custom parsers
and `Validator[T]` functions. Built-in validators cover rune length, ordered
ranges, email, HTTP(S) URLs, patterns and required acceptance. Exact decimals and
application-specific identifiers should use explicit types/parsers rather than
binary floating point. Parser error details are never exposed as display text.

Pass cross-field checks to `New`; they run after every field converts successfully
and return `forms.Errors`, using an empty key for form-wide messages. `Errors`,
`Values` and `Fields` return independent snapshots; `Value[[]string]` also copies
choice lists. Custom pointer/map values and callbacks remain application-owned;
use immutable values and pure, concurrency-safe parsers/validators. Check `Valid`
before acting on cleaned fields. Password/file values are excluded from `Fields`
redisplay; raw `Values` intentionally retains the submission and must not be logged.
The original `forms.Parse`/`Required` API remains compatible.

## Accessible components

Render each `bound.Fields()` entry through `ui/form.Bound(field, uniqueID)` and
`ui/form.Field.Call(props, ui.NoSlots{})` in an authored `.templ` component.
The shared widget supports text/textarea, password, email, URL, number, select,
multiselect, checkbox, radio, date, datetime-local, time, file and hidden controls.
Use a custom parser for local datetime/time inputs; the timestamp field deliberately
requires an explicit offset. Set `Multiple` for directly authored multiselect props;
the bound adapter supplies it automatically.

Labels target controls; radio groups use fieldsets/legends; help/error IDs are
connected with `aria-describedby`, errors use `aria-invalid` and `role=alert`.
Submitted choices and checkboxes preserve state. Text, labels, help, errors and
option values are escaped. Password/file widgets never render a submitted value.
Applications display form-wide errors from `Errors()[""]` in their page layout.

## Stale edits

Add an application-owned `Revision int64` column through a recorded migration,
initializing existing rows to 1. Set `model.Config[T].VersionField = "Revision"`;
do not make Revision editable. New model-form records start at 1. Render
`form.Version(ctx, &record)` as hidden `_version`. Updates compare it while holding
the row lock and increment the revision in the same transaction. A missing or
malformed revision is a validation error; an old revision returns
`model.ErrConflict` and writes nothing. Hooks cannot modify the revision.

Formsets use each row's `<prefix>-<index>-_version` for updates and deletions and
roll back the complete collection on a stale row. Admin renders revision fields
and returns HTTP 409 for conflicts, preserving submitted input. Reload and
explicitly reconcile edits; never silently retry using a fresh token. Every writer
to versioned data must advance the same revision. Existing unversioned models keep
their old behavior until explicitly opted in. Row/collection locking still protects
transactional writes; revisions additionally protect against an old browser form.

## File fields and persistence

`forms/upload.Parse(w, r, Config)` streams multipart bodies into private temporary
files under a mandatory total limit and explicit per-field limits. It bounds text
fields and part counts, rejects duplicate/unknown file fields, sanitizes display
filenames, checks optional extension and sniffed media-type allowlists, and
validates required/clear semantics. The client filename is never a storage key.
Use `ValidateContent` for application-specific inspection (image decoding, document
parsing or a scanning service) before storing. Content-type sniffing is not malware
scanning. Serve untrusted files as attachments with appropriate content headers.

Each `upload.Field` defines Name, MaxBytes, Required, AllowClear, MediaTypes,
Extensions and a trusted ExistingKey loaded by the authorized handler. An empty
file input preserves the existing file. `<name>_clear=on` explicitly removes it;
required files cannot be cleared, and replacement plus clear is rejected. File
inputs cannot be prefilled after errors; users must select the file again.

Always `defer batch.Close()`. `batch.Values` supplies ordinary fields for typed or
model binding. `batch.Save(ctx, store, persist)` generates random storage keys,
stores incoming files, and calls `persist(map[string]upload.Stored)` with keys and
metadata. The callback owns and commits the database transaction (for example,
sets the model's file-key field in BeforeSave and uses a revision-checked update).
Return an error only when the database save did not commit. On failure, new objects
are removed and old ones retained; after success, replaced/cleared old objects are
removed. Objects still referenced by another unchanged field are preserved.

Storage and PostgreSQL are separate systems: this is compensating cleanup, not a
distributed transaction. `*upload.CleanupError` exposes retry keys and whether the
database operation committed. Record and retry failed deletions; do not repeat a
committed database save. For process crashes or an uncertain database commit,
applications need durable outbox/reconciliation appropriate to their store. These
operational realities are explicit, not a claim of cross-system atomic commit.
Batch Save is one-use. Cleanup uses a bounded context independent of request
cancellation. Temporary files are removed by Close or failed-parse cleanup.

For browser multipart POSTs with session CSRF, use the middleware order:

```text
authentication -> upload.Middleware(config) -> sessions.CSRF -> handler
```

Keep the session LoadAndSave wrapper outside that chain. The upload middleware
parses once, makes text values available to CSRF, and closes the batch after the
handler chain. Use `upload.FromRequest(r)` in the handler. Alternatively a direct
Parse handler can sit behind CSRF when the client supplies `X-CSRF-Token`.

## Admin integration

`admin.Config[T].Model` supplies model-form fields, validators/checks, hooks and
VersionField. Display field order must match the explicit model field order when
both are supplied. Otherwise fields are derived from the existing admin allowlist.
`admin.Field.Widget`/Choices choose the shared widgets; listed choices are checked
server-side. `Seed` supplies trusted defaults. Permissions are checked on the
current locked record and the proposed model. Existing routes retain deny-by-default
permissions, scoped lookup and the external authentication/CSRF middleware contract.

The first-party `formsets.AdminInline[P,T](https://github.com/fauzaanu/cicadel/blob/main/docs/AdminConfig[P,T])` adapter goes in
`admin.Config[P].Inlines`. Configure child Set, ForeignKey (Go field name), Label,
Extra, ParentScope and optional field Widgets. Extra rows render ordinary controls;
entirely empty new rows are ignored, and partially filled rows validate normally.
The parent key is derived from the authorized parent model, never POST. Scope,
identity, version, min/max, deletion and protected-field rules remain those of the
formset API. Child errors redisplay beside their row controls.

Parent creation/update and all inline saves share one outer transaction. Any
child error rolls back the parent, its revision and every child mutation. The
adapter uses only public admin/model-form integration contracts; core does not
import the first-party module. Parent deletion follows authored foreign-key/cascade
policies. No implicit related-record cascade is added by the framework.
