esc

Type to search...

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.

siblings-field-array.cljs
(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:

siblings-form.cljs
(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-handlers.cljs
: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:

sortable-field-array.cljs
(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