Iwf Guide
Forms and Validation
Iwf forms are normal HTML forms backed by typed routes and input records. The route decides where the form submits. The input record decides which fields are accepted.
1. Define The Input Record
Create a record for the submitted fields:
record SettingsForm where
constructor MkSettingsForm
email : String
username : String
bio : Maybe String
Derive form fields:
%runElab derive "SettingsForm" [Show, Eq, FormFields]
This generates field values such as:
settingsFormEmailField : FormField SettingsForm "email" String
settingsFormUsernameField : FormField SettingsForm "username" String
When you rename a record field, the generated field value changes too. Stale form field names fail at compile time.
2. Define The Submit Route
A handler with a body record receives decoded form data:
saveSettings : SettingsForm -> ControllerContext -> Response
saveSettings input context =
case validateSettings input of
Left errors => renderValidation context input errors settingsPage
Right valid =>
flashInContext "notice" "Settings saved" context
(seeOther (pathTo settingsRoute))
settingsRoute : RouteSpec NoInput SettingsForm Response
settingsRoute =
put "/settings" |> body SettingsForm |> html
settingsBinding : BoundRoute
settingsBinding =
handledBy settingsRoute saveSettings
For routes with path/query params and body fields, use two records:
createComment : CommentParams -> CommentInput -> ControllerContext -> Response
The first record comes from the URL. The second record comes from the request body.
3. Render The Form
In .idrx, write normal HTML:
settingsForm : SettingsForm -> List ValidationError -> Html
settingsForm input errors =
<form id="settings-form" class="form stack" method="post" action={pathTo settingsRoute}>
{errorSummary errors}
<label class="field">
<span>Email</span>
<input name={settingsFormEmailField} value={input.email} type="email" />
</label>
<label class="field">
<span>Username</span>
<input name={settingsFormUsernameField} value={input.username} />
</label>
<button class="btn-primary" type="submit">Update settings</button>
</form>
render injects the CSRF hidden input into non-GET forms.
Route-backed forms are HTMX-enhanced by default when the .idrx macro can see the target route. Write the form action directly as action={pathTo ...} and Iwf renders the matching HTMX verb attribute:
<form action="/settings" hx-patch="/settings" method="POST">
...
</form>
If the action is a precomputed String, the macro cannot recover the route method or body membership from that string. Keep pathTo visible in the form attribute when you want compile-time field checks and generated HTMX verbs.
Iwf still keeps the native HTML fallback. Browsers without JavaScript only submit GET or POST. For a PUT, PATCH, or DELETE route, Iwf accepts the common method-override pattern:
<form method="post" action="/settings">
<input type="hidden" name="_method" value="PATCH">
...
</form>
The browser still sends POST, but Iwf reads _method and treats the request as PATCH before route matching. Direct forms should spell that hidden field explicitly when they need a no-JavaScript fallback for PUT, PATCH, or DELETE routes.
If you write an HTMX-only control yourself, use the HTMX verb directly:
<button hx-patch="/settings" hx-target="#settings-form">Save</button>
Use hx-put, hx-patch, or hx-delete for the HTMX request. Keep _method only when the same form should also work as normal HTML without JavaScript.
4. Decode And Validate
For simple records:
settingsFormDecoder : FromFormBody SettingsForm
settingsFormDecoder =
derivedFromFormBody settingsFormParams settingsFormFromParams
Useful validation helpers:
requiredStringminLengthmatchesfieldError
Validation returns:
Either (List ValidationError) value
Render errors back to the same page:
renderValidation context input errors settingsPage
Use renderValidationWithFlashes when the failed response should also set flash messages. Submitted values stay in the input record, field errors are rendered, CSRF still works, and boosted HTMX requests stay on the fragment path.
5. Direct Forms Are Canonical
The public surface is literal HTML plus typed values:
action={pathTo route ...}for the route-backed URL.- Generated
FormFieldvalues fornameattributes. - A normal
method,enctype, and optional_methodhidden field when the form needs a browser fallback for non-POST routes.
This keeps the interface inspectable as HTML while still letting Idris check that field names match the route body at compile time.
6. Multipart Forms
Use multipart when the body contains files:
<form method="post" enctype="multipart/form-data" action="/avatar">
<input type="file" name="avatar" />
<button type="submit">Upload</button>
</form>
Route bodies can contain:
UploadedFileMaybe UploadedFileList UploadedFile
Use multipartFile for one file and multipartFilesFor for repeated uploads when you need to inspect raw multipart data.
uploadAvatar : MultipartFormData -> IO (Either String StoredFile)
uploadAvatar formData =
case multipartFile "avatar" formData of
Nothing => pure (Left "missing avatar upload")
Just file =>
storeUploadedFile
(localFileStorage (MkLocalFileStorage "uploads" "/uploads"))
file
7. CSRF
render injects CSRF fields into non-GET forms. Validate submitted forms with:
validateSessionCsrfhandleProtectedFormWith
The CSRF token is stored in the signed session.
For the dependent-type mechanics behind FormField, route body membership, and IDRX form checks, see Dependent Routes and Forms.
Next
Read Database and Migrations.