So far we have not handled any events. We will use data to drive events.
dumdom
, the rendering library we're using supports a global event handler.
Whenever it encounters an event attribute, such as on-click
that is not a
function, the value is instead passed to the global event handler.
First, let's add event capabilities to the DateInput
component:
(d/defcomponent DateInput [props]
[:div {:class (:class props)}
[:input {:class [:input (when (:error? props)
:is-danger)]
:type "text"
:value (:value props)
:placeholder (:placeholder props)
:on-input (:input-actions props)
:on-blur (:blur-actions props)}]
(when (:message props)
[:p {:class [:help (when (:error? props)
:is-danger)]}
(:message props)])])
And here's an example of what the events look like:
(testing "Includes current value"
(is (= (sut/prepare-date-input {:field {:value "2023"}} :field)
{:placeholder "YYYY-MM-DD"
:value "2023"
:blur-actions [[:action/save [:field :validating?] true]]})))
In other words - when the user blurs a field after typing in it, set the field's
:validating?
flag in the global store to true
. That will cause a new render,
and prepare-input-field
will produce an error message that will eventually be
rendered on screen.
This is how to wire this up in dumdom:
(ns form-app.core
(:require [dumdom.core :as d]
[form-app.form :as form]
[form-app.ui.date-form :refer [DateForm]]))
(defn execute-actions [store actions]
(doseq [[action & args] actions]
(apply prn 'Execute action args)
(case action
:action/save (apply swap! store assoc-in args))))
(defn register-actions [store]
(d/set-event-handler!
(fn [e actions]
(execute-actions store actions))))
(defn start [store element]
(register-actions store)
,,,)
If you wanted to, you could add more indirection and dispatch from
execute-actions
, but I find the compact directness of this code to be a
strength. The actions are loosely coupled from the UI by way of
register-actions
, and that's good enough for me.
Input events are most interesting for the data they carry: they provide access
to the user's input. However, that information isn't available until the event
fires. Since we're using data, we'll have to access that information with late
binding. We can use a placeholder in the action data that will be replaced by
the actual value when the event triggers. Like this:
(testing "Prepares field with placeholder"
(is (= (sut/prepare-date-input {} :field)
{:placeholder "YYYY-MM-DD"
:value ""
:input-actions [[:action/save [:field :value] :event/target.value]]})))
And then we will need to fix our wiring to make sure that :event/target.value
is replaced with the actual target value:
(ns form-app.core
(:require [clojure.walk :as walk]
[dumdom.core :as d]
[form-app.form :as form]
[form-app.ui.date-form :refer [DateForm]]))
(defn execute-actions [store actions]
,,,)
(defn register-actions [store]
(d/set-event-handler!
(fn [e actions]
(->> actions
(walk/postwalk
(fn [x]
(if (= :event/target.value x)
(some-> e .-target .-value)
x)))
(execute-actions store)))))
clojure.walk/postwalk
ships with Clojure and provides a snappy solution to our
problem. This will only happen when events trigger, and on very small datasets,
and will be plenty fast.
Now we're ready to review the entire prepare-date-input
function:
(defn prepare-date-input [state k]
(let [{:keys [value validating?]} (k state)
message (when validating?
(validate-date value))]
(cond-> {:placeholder "YYYY-MM-DD"
:value (or value "")
:input-actions
(->> [[:action/save [k :value] :event/target.value]
(when (and validating?
(or (empty? value) (not message)))
[:action/save [k :validating?] false])]
(remove nil?))}
message
(assoc :message message
:error? (boolean message))
(and (not validating?) value)
(assoc :blur-actions [[:action/save [k :validating?] true]]))))
I left out the validations, as the specifics are not interesting, but you can
read all the code on
github.
This function captures all the interesting behavior for the field: It does not
validate as the user types. When the field is blurred, an error is displayed if
necessary. The error will be cleared immediately - as the user types - and when
it has been cleared, no new error will be produced until the next blur. This is
tricky stuff to get right, but there are a bunch of
tests,
and those are easy to write, since it's just a plain old pure function.