Skip to content

Commit

Permalink
clinic: refactor validation logic in create patient form
Browse files Browse the repository at this point in the history
links to #2
  • Loading branch information
ashutoshgngwr committed Oct 18, 2023
1 parent 00f3a36 commit 8222af9
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 78 deletions.
1 change: 1 addition & 0 deletions clinic/shadow-cljs.edn
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{:source-paths ["src/cljc" "src/cljs"]
:dependencies [[bidi "2.1.6"]
[binaryage/devtools "1.0.7"]
[cljs-ajax "0.7.5"]
[day8.re-frame/http-fx "0.2.4"]
[kibu/pushy "0.3.8"]
Expand Down
47 changes: 23 additions & 24 deletions clinic/src/cljs/clinic/components.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -28,31 +28,30 @@
"Logout"]])]]
(r/children this))))

(defn text-field [_ _ _ _ validation-fn]
(let [valid? (r/atom true)]
(fn [name label placeholder error-msg _]
[:div {:class ["w-full" "flex" "flex-col" "gap-2"]}
[:label {:for name
:class ["block" "uppercase" "tracking-wide" "text-gray-600"
"text-xs" "font-bold"]}
label]
(defn text-field []
(let [{name :name
label :label
placeholder :placeholder
error-msg :error-msg
touched? :touched?
invalid? :invalid?} (r/props (r/current-component))]
[:div {:class ["w-full" "flex" "flex-col" "gap-2"]}
[:label {:for name
:class ["block" "uppercase" "tracking-wide" "text-gray-600"
"text-xs" "font-bold"]}
label]

[:input {:id name
:name name
:placeholder placeholder
:class ["appearance-none" "block" "w-full" "bg-gray-200"
"text-gray-700" "border" "border-gray-200"
"rounded" "py-3" "px-4" "leading-tight"
"focus:outline-none" "focus:bg-white"
"focus:border-gray-500"]
:on-change #(->> %
(.-target)
(.-value)
(validation-fn)
(reset! valid?))}]
[:p {:class [(if @valid? "invisible" "visible")
"text-red-500" "text-xs" "italic"]}
error-msg]])))
[:input {:id name
:name name
:placeholder placeholder
:class ["appearance-none" "block" "w-full" "bg-gray-200"
"text-gray-700" "border" "border-gray-200"
"rounded" "py-3" "px-4" "leading-tight"
"focus:outline-none" "focus:bg-white"
"focus:border-gray-500"]}]
[:p {:class [(if (and touched? invalid?) "visible" "invisible")
"text-red-500" "text-xs" "italic"]}
error-msg]]))

(defn select-field [name label default-value options]
[:div {:class ["w-full" "flex" "flex-col" "gap-2"]}
Expand Down
28 changes: 21 additions & 7 deletions clinic/src/cljs/clinic/utils.cljs
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
(ns clinic.utils)
(ns clinic.utils
(:require [clojure.spec.alpha :as s]))

(defn form-data->map
"Converts DOM FormData to a Clojure map. Also keywordizes keys and filters
out empty values from the resulting map."
[form-data]
"Converts DOM FormData to a Clojure map. Also keywordizes keys in the
resulting map and then removes `optional-keyset` from it if the corresponding
value is empty."
[form-data optional-keyset]
(->> form-data
(.entries)
(map vec)
(remove #(empty? (second %))) ; remove empty fields
(reduce #(assoc %1 (keyword (first %2)) (second %2)) {})))
(map (fn [[k v]] [(keyword k) v]))
(remove #(and (contains? optional-keyset (first %))
(empty? (second %)))) ; remove empty fields that are optional
(into {})))

(defn invalid-keys
"Returns a set of keys whose values don't conform to the given `spec` for the
given `data`."
[spec data]
(->> data
(s/explain-data spec)
(::s/problems)
(map :in)
(flatten)
(set)))
109 changes: 62 additions & 47 deletions clinic/src/cljs/clinic/views/create_patient.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
[clinic.router :as router]
[clinic.specs.patient :as specs]
[clinic.utils :as u]
[clojure.spec.alpha :as s]
[day8.re-frame.http-fx]
[re-frame.core :as rf]
[reagent.core :as r]))
Expand Down Expand Up @@ -33,26 +32,45 @@
(rf/reg-sub ::submitting-form :-> ::submitting-form)
(rf/reg-sub ::submit-form-error-code :-> ::submit-form-error-code)

(defn form-data [form]
(-> form
(js/FormData.)
(u/form-data->map #{:marital-status :email :phone})))

(defn find-invalid-keys [form]
(->> form
(form-data)
(u/invalid-keys ::specs/create-params)))

(defn root []
(let [form-valid? (r/atom nil)
(let [form-ref (atom nil)
touched? (r/atom #{})
invalid? (r/atom #{})
submitting? (rf/subscribe [::submitting-form])
submit-error-code (rf/subscribe [::submit-form-error-code])]
(fn []
[:section {:class ["flex" "flex-col" "gap-12"]}
[components/heading-2 "Add a Patient"]
[:form {:method "POST"
[:form {:ref (partial reset! form-ref)
:method "POST"
:action "/api/v1/patients/"
:class ["w-full" "flex" "flex-col" "gap-4"]
:on-blur #(do (swap! touched? conj (-> %
(.-target)
(.-id)
(keyword)))
(reset! invalid? (find-invalid-keys @form-ref)))
:on-change #(do (swap! touched? conj (-> %
(.-target)
(.-id)
(keyword)))
(reset! invalid? (find-invalid-keys @form-ref)))
:on-submit #(do (.preventDefault %)
(let [form-data (-> %
(.-target)
(js/FormData.)
(u/form-data->map))]
(->> form-data
(s/valid? ::specs/create-params)
(reset! form-valid?))

(when @form-valid?
(let [form-data (form-data @form-ref)]
;; touch all fields and revalidate data.
(reset! touched? (set (keys form-data)))
(reset! invalid? (find-invalid-keys @form-ref))
(when (empty? @invalid?)
(rf/dispatch [::submit-form form-data]))))}

(when @submit-error-code
Expand All @@ -62,31 +80,28 @@
correct?"
"There was an error while adding patient. Please try again!")])

(when (false? @form-valid?)
[components/danger-alert "Missing required fields or invalid input!"])

[:div {:class ["w-full" "flex" "flex-col" "md:flex-row" "gap-8" "md:gap-12"]}
[components/text-field
:first-name
"First Name *"
"Jane"
"Please enter a valid first name!"
(partial s/valid? ::specs/first-name)]

[components/text-field
:last-name
"Last Name *"
"Doe"
"Please enter a valid last name!"
(partial s/valid? ::specs/last-name)]]
[components/text-field {:name :first-name
:label "First Name *"
:placeholder "Jane"
:error-msg "Please enter a valid first name!"
:touched? (contains? @touched? :first-name)
:invalid? (contains? @invalid? :first-name)}]

[components/text-field {:name :last-name
:label "Last Name *"
:placeholder "Doe"
:error-msg "Please enter a valid last name!"
:touched? (contains? @touched? :last-name)
:invalid? (contains? @invalid? :last-name)}]]

[:div {:class ["w-full" "flex" "flex-col" "md:flex-row" "gap-8" "md:gap-12"]}
[components/text-field
:birth-date
"Date of Birth *"
"1999-12-30"
"Please enter a valid date of birth in YYYY-MM-DD format!"
(partial s/valid? ::specs/birth-date)]
[components/text-field {:name :birth-date
:label "Date of Birth *"
:placeholder "1999-12-30"
:error-msg "Please enter a valid date of birth in YYYY-MM-DD format!"
:touched? (contains? @touched? :birth-date)
:invalid? (contains? @invalid? :birth-date)}]

[components/select-field
:gender
Expand All @@ -107,18 +122,18 @@
["Widowed" "W"]
["Unknown" "UNK"]]]

[components/text-field
:email
"Email"
"[email protected]"
"Please enter a valid email!"
(partial s/valid? ::specs/email)]

[components/text-field
:phone
"Phone"
"0000-000-000"
"Please enter a valid phone!"
(partial s/valid? ::specs/phone)]
[components/text-field {:name :email
:label "Email"
:placeholder "[email protected]"
:error-msg "Please enter a valid email!"
:touched? (contains? @touched? :email)
:invalid? (contains? @invalid? :email)}]

[components/text-field {:name :phone
:label "Phone"
:placeholder "0000-000-000"
:error-msg "Please enter a valid phone!"
:touched? (contains? @touched? :phone)
:invalid? (contains? @invalid? :phone)}]

[components/button "submit" "Add Patient" @submitting?]]])))

0 comments on commit 8222af9

Please sign in to comment.