The Problem
Validation timing is harder than the validation itself. When do you show errors? On every keystroke? On blur? Only after the first submit attempt? What about server-side checks like "email already taken" that need an HTTP round-trip? And how do you prevent submission while any of these checks are still pending?
Fork handles all of this. You provide a pure function that maps values to errors. Fork decides when to evaluate it, when to display errors (after touch), and when to block submission (while errors exist). For server-side validation, there's a separate flow with built-in debounce, waiting states, and error injection.
Client-Side Validation
Pass any side-effect-free function via :validation.
It receives the values map and returns errors in whatever shape your
validation library produces. Here's an example using
Vlad:
(ns app.core
(:require
[fork.re-frame :as fork]
[re-frame.core :as rf]
[vlad.core :as vlad]))
(def validation
(vlad/join (vlad/attr ["name"]
(vlad/chain
(vlad/present)
(vlad/length-in 3 15)))
(vlad/attr ["password"]
(vlad/chain
(vlad/present)
(vlad/length-over 7)))))
[fork/form {:path [:form]
:form-id "signup"
:validation #(vlad/field-errors validation %)
:prevent-default? true
:on-submit #(rf/dispatch [:submit-handler %])}
(fn [{:keys [values errors touched
handle-change handle-blur
submitting? handle-submit]}]
[:form
{:id "signup"
:on-submit handle-submit}
[:input
{:name "name"
:value (values "name")
:on-change handle-change
:on-blur handle-blur}]
(when (touched "name")
[:div.error (first (get errors (list "name")))])
[:input
{:name "password"
:type "password"
:value (values "password")
:on-change handle-change
:on-blur handle-blur}]
(when (touched "password")
[:div.error (first (get errors (list "password")))])
[:button
{:type "submit"
:disabled submitting?}
"Sign Up"]])]
The key detail: touched is a function that
returns truthy when the user has blurred a field (or when
:attempted-submissions is greater than zero).
This means errors exist in the errors map
from the first keystroke, but you only render them after the field
loses focus. The user gets immediate feedback without being nagged
before they finish typing.
Custom Validation
You don't need a library. Any function that takes values and returns a map of errors works:
(defn my-validation [values]
(cond-> {}
(empty? (get values "email"))
(assoc "email" "Email is required")
(< (count (get values "password" "")) 8)
(assoc "password" "Must be at least 8 characters")))
[fork/form {:validation my-validation
:initial-values {"email" "" "password" ""}
...}
(fn [{:keys [values errors touched handle-change handle-blur]}]
[:div
[:input
{:name "email"
:value (values "email")
:on-change handle-change
:on-blur handle-blur}]
(when (touched "email")
[:div.error (get errors "email")])])]Password Confirmation
Cross-field validation (like password confirmation) works naturally because the validation function receives all values at once:
(def form-validation
(fn [password]
(vlad/join
(vlad/attr ["password"]
(vlad/chain (vlad/present)
(vlad/length-in 6 128)))
(vlad/attr ["confirm-password"]
(vlad/chain
(vlad/equals-value
password
{:message
"Confirm Password must be same as password"}))))))
[fork/form {:validation
#(vlad/field-errors
(form-validation (get % "password"))
%)
...}
...]
The validation function closes over the current password value from
the values map. Vlad's equals-value compares
the confirm field against it. Every time the values change, Fork
re-evaluates the validation function with the latest state.
Server-Side Validation
For validations that require a server round-trip (checking if an email
is already taken, verifying a username), use
send-server-request. This is covered in depth
in the Server Requests
guide, but here's the key idea: server errors and client errors coexist.
Fork blocks submission while either type has unresolved errors, and
while any server request is in a waiting state.
;; In your form component, both error sources are available:
(fn [{:keys [errors server-errors touched]}]
[:div
[:input {:name "email" ...}]
(when (touched "email")
[:div.error
(or (get errors "email")
(get server-errors "email"))])])Next Steps
- Server Requests — debounced server-side validation and error injection
- Field Arrays — dynamic field groups with validation
- API Reference — every handler and option