The Problem
Some forms have a variable number of entries. A user might have zero siblings or five. A recipe might need three ingredients or thirty. You can't hardcode the field count. You need dynamic groups that can be added, removed, and reordered, with touched tracking that survives additions and deletions without getting out of sync.
Fork's field arrays handle this. Each array is a vector of maps inside the form values. Fork provides insert, remove, and drag-and-drop handlers that keep the state consistent, including correctly shifting touched indices when items are removed.
Basic Field Array
A field array requires two components: the parent form and a separate Reagent component for the array rows. The separation is important: if the array rows live inside an anonymous function, they lose focus on every state change.
(defn siblings-field-array
[_props
{:fieldarray/keys [fields insert remove
handle-change handle-blur]}]
[:<>
(map-indexed
(fn [idx field]
^{:key idx}
[:div
[:div
[:label "First Name"]
[:input
{:name "first-name"
:value (get field "first-name")
:on-change #(handle-change % idx)
:on-blur #(handle-blur % idx)}]]
[:div
[:label "Last Name"]
[:input
{:name "last-name"
:value (get field "last-name")
:on-change #(handle-change % idx)
:on-blur #(handle-blur % idx)}]]
[:button
{:type "button"
:on-click #(when (> (count fields) 1)
(remove idx))}
"Remove"]])
fields)
[:button
{:type "button"
:on-click #(insert {"first-name" "" "last-name" ""})}
"Add Sibling"]])Then wire it into the parent form:
(defn siblings-form []
[fork/form
{:on-submit #(js/alert (:values %))
:initial-values
{"siblings" [{"first-name" "" "last-name" ""}]}
:prevent-default? true}
(fn [{:keys [handle-submit] :as props}]
[:form
{:on-submit handle-submit}
[:h2 "Siblings"]
[fork/field-array {:props props
:name "siblings"}
siblings-field-array]
[:button {:type "submit"} "Submit"]])])Field Array Handlers
The field array component receives two arguments: the form props (from the parent) and a map of namespaced handlers:
:fieldarray/name ;; the name chosen for this array
:fieldarray/options ;; custom props passed via :options key
:fieldarray/fields ;; vector of maps (the array data)
:fieldarray/touched ;; (touched idx :my-input)
:fieldarray/insert ;; (insert {"field" "default-value"})
:fieldarray/remove ;; (remove idx)
:fieldarray/handle-change ;; (handle-change evt idx)
:fieldarray/handle-blur ;; (handle-blur evt idx)
:fieldarray/set-handle-change ;; programmatic value changes
:fieldarray/set-handle-blur ;; programmatic blur changes
Notice that handle-change and
handle-blur take an extra
idx argument compared to the top-level
form handlers. This tells Fork which row in the array the event
belongs to.
Drag and Drop
Field arrays support drag-and-drop reordering for top-level arrays. Fork provides all the HTML5 drag event handlers. You merge them into each row's wrapper element:
(defn sortable-field-array
[_props
{:fieldarray/keys [fields insert remove
handle-change handle-blur
drag-and-drop-handlers
next-droppable-target?
prev-droppable-target?]}]
[:<>
(map-indexed
(fn [idx field]
^{:key idx}
[:div
(merge
{:class (cond
(next-droppable-target? "items" idx)
"border-bottom-highlight"
(prev-droppable-target? "items" idx)
"border-top-highlight")}
(drag-and-drop-handlers "items" idx))
[:input
{:name "label"
:value (get field "label")
:on-change #(handle-change % idx)
:on-blur #(handle-blur % idx)}]
[:button
{:type "button"
:on-click #(remove idx)}
"Remove"]])
fields)
[:button
{:type "button"
:on-click #(insert {"label" ""})}
"Add Item"]]) drag-and-drop-handlers returns a map of
:draggable,
:on-drag-start,
:on-drag-end,
:on-drag-over,
:on-drag-enter, and
:on-drop attributes. The helper functions
next-droppable-target? and
prev-droppable-target? let you style the
drop indicator based on the drag direction.
Next Steps
- Server Requests — server-side validation with debounce
- API Reference — every handler and option