-
Notifications
You must be signed in to change notification settings - Fork 1
/
form.cljs
155 lines (140 loc) · 7.66 KB
/
form.cljs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
(ns ez-wire.form
(:require [ez-wire.util :as util]
[ez-wire.form.elements]
[ez-wire.form.protocols :as form.protocols :refer [valid? get-error-message]]
[ez-wire.form.list :as form.list]
[ez-wire.form.paragraph :as form.paragraph]
[ez-wire.form.table :as form.table]
[ez-wire.form.template :as form.template]
[ez-wire.form.validation :as form.validation]
[ez-wire.form.wire :as form.wire]
[ez-wire.form.wizard :as form.wizard]
[reagent.core :refer [atom] :as r]
[re-frame.core :as rf]))
(rf/reg-sub ::error (fn [db [_ id field-name]]
(get-in db [::error id field-name] [])))
(rf/reg-sub ::on-valid (fn [db [_ id]]
(get-in db [::on-valid id] ::invalid)))
(rf/reg-sub ::form.wizard/current-step (fn [db [_ id]]
(get-in db [::wizard id :current-step])))
(rf/reg-event-db ::error (fn [db [_ id field-name errors]]
(assoc-in db [::error id field-name] errors)))
(rf/reg-event-db ::on-valid (fn [db [_ id new-state]]
(assoc-in db [::on-valid id] new-state)))
(rf/reg-event-db ::form.wizard/current-step (fn [db [_ id step]]
(assoc-in db [::wizard id :current-step] step)))
(defmulti ^:private get-field-fn (fn [field]
(let [{:keys [element adapter]} field]
(cond adapter :adapt-fn
(fn? element) :fn
(util/reagent-native-wrapper? element) :fn
(keyword? element) :keyword
:else nil))))
(defmethod get-field-fn :fn [field]
(:element field))
(defmethod get-field-fn :adapt-fn [{:keys [adapter] :as field}]
(adapter field))
(defmethod get-field-fn :default [field]
(:element field))
(defn- finalize-error-message [context error-message]
(if (fn? error-message)
(with-meta (error-message context) {::by-fn? true})
error-message))
(defn- get-error-messages [{:keys [validation] :as field} value form]
(let [v (if (sequential? validation) validation [validation])
context {:field field
:value value
:form form}]
(->> v
(remove #(valid? % value form))
(map (comp #(finalize-error-message context %)
#(get-error-message % value form)))
(remove nil?))))
(defn- get-validation-errors [form old-state]
(fn [out [k v]]
(let [field (get-in form [:fields k])]
;; update? is used to determine if we should do updates to errors
;; we still need all the errors for on-valid to properly fire
(let [update? (not= v (get old-state k))]
(if (valid? (:validation field) v form)
;; if the validation is valid we change it to hold zero errors
(conj out [k update? []])
;; if the validation is invalid we give an explanation to
;; what's wrong
(conj out [k update? (get-error-messages field v form)]))))))
(defn- get-external-errors [form field-errors]
(reduce (fn [out [k update? errors]]
(if-let [external-errors (get-in @(:extra form) [k :field-errors])]
(let [trimmed-external-errors (remove #(valid? % nil nil) external-errors)]
;; run a check if we need to update the external errors
;; because some of them are being removed
(if-not (= (count external-errors)
(count trimmed-external-errors))
(swap! (:extra form) assoc-in [k :field-errors] trimmed-external-errors))
(conj out [k update? (->> trimmed-external-errors
(map #(get-error-message % nil nil))
(into errors))]))
(conj out [k update? errors])))
[] field-errors))
(defn- add-validation-watcher
"Add validation checks for the RAtom as it changes"
[form]
(let [{{on-valid :on-valid} :options} form]
(add-watch (:data form) (str "form-watcher-" (:id form))
(fn [_ _ old-state new-state]
;; get all errors for all fields
(let [field-errors (->> (reduce (get-validation-errors form old-state) [] new-state)
(get-external-errors form))]
;; update the RAtoms for the error map
(doseq [[k update? errors] field-errors]
(when update?
(rf/dispatch [::error (:id form) k errors])
(reset! (get-in form [:errors k]) errors)))
;; if there are no errors then the form is valid and we can fire off the function
(let [valid? (every? empty? (map last field-errors))
to-send (if valid? new-state ::invalid)]
(when (fn? on-valid)
(on-valid to-send))
(rf/dispatch [::on-valid (:id form) to-send])))))))
(defn- get-default-value [data name field]
(get data name (:value field)))
(defrecord Form [fields field-ks options id errors data meta])
(defn form [fields form-options override-options data]
;; do the conform here as conform can change the structure of the data
;; that comes out in order to show how it came to that conclusion (spec/or for example)
(let [options (merge {:id (util/gen-id)} form-options override-options)
map-fields (->> fields
(map (fn [{:keys [name id error-element] :as field}]
[name (assoc field
:field-fn (get-field-fn field)
:value (get-default-value data name field)
:error-element (or error-element
ez-wire.form.elements/error-element)
;; always generate id in the form so we
;; can reference it later
:id (or id (util/gen-id)))]))
(into (array-map)))
-data (reduce (fn [out [name field]]
(assoc out name (get-default-value data name field)))
{} map-fields)
errors (reduce (fn [out [name _]]
(assoc out name (atom [])))
{} map-fields)
form (map->Form {:fields map-fields
;; field-ks control which fields are to be rendered for
;; everything form supports with the exception of wiring
:field-ks (mapv :name fields)
:options options
:id (:id options)
:extra (atom {})
:errors errors
:data (atom {})})]
(add-validation-watcher form)
;; run validation once before we send back our form
(reset! (:data form) -data)
form))
(def as-list (form.wizard/wizard form.list/as-list))
(def as-paragraph (form.wizard/wizard form.paragraph/as-paragraph))
(def as-table (form.wizard/wizard form.table/as-table))
(def as-template (form.wizard/wizard form.template/as-template))
(def as-wire form.wire/as-wire)