A way better approach for form validation. Feels good now.
This commit is contained in:
@@ -5,7 +5,9 @@
|
||||
[react :as react]
|
||||
[reagent.core :as r]
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.status :as status]))
|
||||
[auto-ap.status :as status]
|
||||
[malli.core :as m]
|
||||
[malli.error :as me]))
|
||||
|
||||
(defonce ^js/React.Context form-context (react/createContext "default"))
|
||||
(def ^js/React.Provider Provider (. form-context -Provider))
|
||||
@@ -15,16 +17,46 @@
|
||||
(def ^js/React.Provider FormScopeProvider (. form-scope-context -Provider))
|
||||
(def ^js/React.Consumer FormScopeConsumer (. form-scope-context -Consumer))
|
||||
|
||||
(defn builder [{:keys [can-submit data-sub change-event submit-event id fullwidth?] :as z}]
|
||||
(let [data-sub (or data-sub [::forms/form id])
|
||||
change-event (or change-event [::forms/change id])
|
||||
{:keys [data error] form-key :id} @(re-frame/subscribe data-sub)
|
||||
status @(re-frame/subscribe [::status/single id])
|
||||
can-submit (if can-submit @(re-frame/subscribe can-submit)
|
||||
true)]
|
||||
(defn valid-field? [problems field-path]
|
||||
(not (get-in (me/humanize problems) field-path)))
|
||||
|
||||
(defn spec-error-message [problems field-path error-messages]
|
||||
(-> (me/humanize problems
|
||||
{:errors (merge (-> me/default-errors
|
||||
(assoc ::m/missing-key {:error/message "Required"}
|
||||
::m/invalid-type {:error/fn
|
||||
(fn [a b]
|
||||
(if (nil? (:value a))
|
||||
"Required"
|
||||
"Invalid"))}))
|
||||
error-messages)})
|
||||
(get-in field-path)
|
||||
first))
|
||||
|
||||
(defn builder [{:keys [value on-change can-submit data-sub error-messages change-event submit-event id fullwidth? schema] :as z}]
|
||||
(when (and change-event on-change)
|
||||
(throw "Either the form is to be managed by ::forms, or it should have value and on-change passed in"))
|
||||
(let [data-sub (or data-sub [::forms/form id])
|
||||
change-event (when-not on-change
|
||||
(or change-event [::forms/change id]))
|
||||
{:keys [data error visited] form-key :id} @(re-frame/subscribe data-sub)
|
||||
data (or value data)
|
||||
status @(re-frame/subscribe [::status/single id])
|
||||
can-submit (if can-submit @(re-frame/subscribe can-submit)
|
||||
true)
|
||||
problems (when schema
|
||||
(m/explain schema data))]
|
||||
|
||||
|
||||
(r/create-element Provider #js {:value #js {:can-submit can-submit
|
||||
:error-messages (or error-messages
|
||||
nil)
|
||||
:on-change on-change
|
||||
:change-event change-event
|
||||
:blur-event [::forms/visited id]
|
||||
:visited visited
|
||||
:submit-event submit-event
|
||||
:problems problems
|
||||
:error error
|
||||
:status status
|
||||
:id id
|
||||
@@ -42,6 +74,30 @@
|
||||
(r/children (r/current-component)))]
|
||||
))))
|
||||
|
||||
(defn virtual-builder []
|
||||
(let [key (r/atom (random-uuid))]
|
||||
(fn [{:keys [value on-change can-submit error-messages fullwidth? schema]}]
|
||||
(let [data-sub [::forms/form @key]
|
||||
{:keys [data error visited]} @(re-frame/subscribe data-sub)
|
||||
data (or value data)
|
||||
problems (when schema
|
||||
(m/explain schema data))]
|
||||
(r/create-element Provider #js {:value #js {:can-submit can-submit
|
||||
:error-messages (or error-messages
|
||||
nil)
|
||||
:on-change on-change
|
||||
:blur-event [::forms/visited @key]
|
||||
:visited visited
|
||||
:problems problems
|
||||
:error error
|
||||
:id @key
|
||||
:data data
|
||||
:fullwidth? fullwidth?}}
|
||||
(r/as-element
|
||||
^{:key @key}
|
||||
(into [:<>]
|
||||
(r/children (r/current-component)))))))))
|
||||
|
||||
(defn raw-field []
|
||||
(let [[child] (r/children (r/current-component))]
|
||||
[:> Consumer {}
|
||||
@@ -65,10 +121,91 @@
|
||||
(assoc-in [1 :subscription] (aget consume-form "data"))
|
||||
(assoc-in [1 :event] (aget consume-form "change-event")))]))]))]))
|
||||
|
||||
(defn change-handler [path re-frame-change-event event-or-value]
|
||||
(re-frame/dispatch (-> re-frame-change-event
|
||||
(conj path)
|
||||
(conj (if-let [target (some-> event-or-value (aget "target"))]
|
||||
(aget target "value")
|
||||
event-or-value)))))
|
||||
|
||||
(defn form-change-handler [data path on-change event-or-value]
|
||||
(on-change (assoc-in data path (if-let [target (some-> event-or-value (aget "target"))]
|
||||
(aget target "value")
|
||||
event-or-value))
|
||||
data))
|
||||
|
||||
(defn blur-handler [path re-frame-blur-event _]
|
||||
(re-frame/dispatch (-> re-frame-blur-event
|
||||
(conj path))))
|
||||
|
||||
(defn raw-error-v2 [{:keys [field]}]
|
||||
[:> Consumer {}
|
||||
(fn [consume-form]
|
||||
(r/as-element
|
||||
[:> FormScopeConsumer {}
|
||||
(fn [form-scope]
|
||||
(r/as-element
|
||||
(let [full-field-path (cond
|
||||
(sequential? field)
|
||||
(into form-scope field)
|
||||
|
||||
field
|
||||
(conj form-scope field)
|
||||
|
||||
:else
|
||||
nil)
|
||||
visited? (get (aget consume-form "visited") full-field-path)]
|
||||
(when-let [error-message (and
|
||||
visited?
|
||||
(spec-error-message (aget consume-form "problems") full-field-path (aget consume-form "error-messages")))]
|
||||
[:div
|
||||
[:p.help.has-text-danger error-message]]))))]))])
|
||||
|
||||
(defn raw-field-v2 [{:keys [field]}]
|
||||
(let [[child] (r/children (r/current-component))]
|
||||
[:> Consumer {}
|
||||
(fn [consume-form]
|
||||
(r/as-element
|
||||
[:> FormScopeConsumer {}
|
||||
(fn [form-scope]
|
||||
(r/as-element
|
||||
(update child 1 (fn [child-props]
|
||||
(let [
|
||||
full-field-path (cond
|
||||
(sequential? field)
|
||||
(into form-scope field)
|
||||
|
||||
field
|
||||
(conj form-scope field)
|
||||
|
||||
:else
|
||||
nil)
|
||||
visited? (get (aget consume-form "visited") full-field-path)
|
||||
value (get-in (aget consume-form "data") full-field-path)
|
||||
on-change (aget consume-form "on-change")]
|
||||
(-> child-props
|
||||
(assoc :on-change
|
||||
(if on-change
|
||||
(partial form-change-handler (aget consume-form "data") full-field-path (aget consume-form "on-change"))
|
||||
(partial change-handler full-field-path (aget consume-form "change-event")))
|
||||
|
||||
:on-blur (partial blur-handler full-field-path (aget consume-form "blur-event"))
|
||||
:value value)
|
||||
(update :class (fn [class]
|
||||
(str class
|
||||
(cond
|
||||
(not visited?)
|
||||
""
|
||||
(not (valid-field? (aget consume-form "problems") full-field-path))
|
||||
" is-danger"
|
||||
:else
|
||||
"is-success"))))))))))]))]))
|
||||
|
||||
(defn with-scope [{:keys [scope]}]
|
||||
(r/create-element FormScopeProvider #js {:value scope}
|
||||
(r/as-element (into [:<>]
|
||||
(r/children (r/current-component))))))
|
||||
|
||||
(defn vertical-control [{:keys [is-small? required?]}]
|
||||
(let [[label & children] (r/children (r/current-component))]
|
||||
[:> Consumer {}
|
||||
@@ -99,6 +236,24 @@
|
||||
label)]))
|
||||
[:div.control [raw-field {} child]]]))]))
|
||||
|
||||
(defn field-v2 []
|
||||
(let [props (r/props (r/current-component))
|
||||
[label child] (r/children (r/current-component))]
|
||||
[:> Consumer {}
|
||||
(fn [consume]
|
||||
(r/as-element
|
||||
[:div.field
|
||||
(when label
|
||||
(if (aget consume "fullwidth?")
|
||||
[:p.help label]
|
||||
[:label.label
|
||||
(if (:required? props)
|
||||
[:span label [:span.has-text-danger " *"]]
|
||||
label)]))
|
||||
[:div.control [raw-field-v2 props child]]
|
||||
[:div
|
||||
[raw-error-v2 {:field (:field props)}]]]))]))
|
||||
|
||||
(defn horizontal-control []
|
||||
(let [[label & children] (r/children (r/current-component))]
|
||||
[:div.field.is-horizontal
|
||||
|
||||
Reference in New Issue
Block a user