I often find myself doing things like this when working with Reagent apps.
(defn create-account [user-details error-ref]
(let [validation-errors (do-validations user-details)]
(if validation-errors
(reset! error-ref validation-errors)
(finish-create))))
(defn component []
(let [user-details (atom {:email "" :password ""})
ui-errors (atom nil)]
(fn []
[:div.Page
(if @ui-errors
[:p @ui-errors])
[:input {:type "text" :placeholder "EMAIL" :on-change #(swap! user-details conj {:email (-> % .-target .-value)})}]
[:input {:type "password" :placeholder "PASSWORD" :on-change #(swap! user-details conj {:password (-> % .-target .-value)})}]
[:button.bottomMargin {:on-click #(create-account @user-details ui-errors)} "Submit"]])))
Where I'll set up an atom that handles storing any errors that happen and pass this into my signup flow. This is pretty simple, as a I can update the atom with any errors and show them to the user.
My concern is that this doesn't feel quite functional. I know I have this atom being updated in my block, and that feels like a side effect that ruins the purity of the function. I also dislike the need to pass the atom into my function for the sole purpose of being updated in an error state.
I suppose I will have to have side effect that at some point in my code if I want to show an error to my users, but I wanted to see if there were any better or accept patterns for doing so. I feel like my implantation works, but isn't quite as functional as I like and could potentially be hard to maintain if the flow for processing or validating the data grew.
Put state in a single atom in it's own namespace like this. Warning: I haven't actually tested this code.
(ns my.app.db
(:require [reagent.ratom :as r]))
(def default-db {})
(def my-db (r/atom default-db))
(defn init-db! []
(reset! my-db default-db))
(defn new-account [user]
(let [validation-errors (do-validation user)]
(if validation-errors
{:valid? false :errors validation-errors}
{:valid? true :user user})))
(defn create-account! [user]
(swap! my-db assoc :account (new-account user)))
Then in you UI code:
(ns my.app.ui
(:require [my.app.db :as db]))
(defn user-form [{:keys [valid? user errors]}]
(fn []
[:div.Page
(if-not valid?
[:p errors])
[:input {:type "text" :placeholder "EMAIL"
:on-change #(swap! db/my-db assoc-in [:account :user :email] (-> % .-target .-value))
:default-value (:email user)}
[:input {:type "password" :placeholder "PASSWORD"
:on-change #(swap! db/my-db assoc-in [:account :user :password] (-> % .-target .-value))
:default-value (:password user)}]
[:button.bottomMargin {:on-click #(db/create-account! user)} "Submit"]]]))
(defn parent-component []
(let [db @db/my-db]
(fn []
[user-form (:account db)])))
You probably also need to add a check if form is dirty, add another property to :account map, so that you don't try to display errors if user is nil. But things like this can get messy quickly, so I would advise you to use something like fork for form management, it works nicely with reagent.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With