This guide demonstrates how to integrate Commando with Reagent, using a practical form component example for managing car data.
Let's start with a typical Reagent form containing two fields for describing a car (e.g., Mazda MX-5). Typically, we store form state in a reagent/atom
:
(defn form-component []
(let [state (reagent.core/atom {:car-model ""
:car-specs ""})]
(fn []
[:div
[:div "Car model"
[:input {:defaultValue (:car-model @state)
:on-change #(swap! state assoc :car-model (.. % -target -value))}]]
[:div "Car Specs"
[:input {:defaultValue (:car-specs @state)
:on-change #(swap! state assoc :car-specs (.. % -target -value))}]]
[:input {:type "button"
:value "submit"
:on-click #(js/console.log @state)}]])))
This is a standard Reagent controlled form. The state atom holds both fields. All logic—validation, transformation, etc. It typically implemented as separate functions or inline handlers.
Usually, the next step is to create functions for validation, normalization, and data processing before submitting. The key idea behind Commando is to "soak up" all this logic into an Instruction — a declarative data structure representing dependencies and transformations.
With Commando, we pass the state atom directly into the instruction. All dependencies and transformations are transparent, and changes in state automatically trigger reactive updates.
(require '[commando.core :as commando])
(require '[commando.commands.builtin :as commands-builtin])
(defn form-component []
(let [state (reagent.core/atom {:car-model ""
:car-specs ""})
state-instruction
(reagent.core/track
(fn []
(commando/execute
[commands-builtin/command-mutation-spec
commands-builtin/command-from-spec
commands-builtin/command-fn-spec]
{:state @state
:validation-car-model
{:commando/mutation :ui/validate
:value {:commando/from [:state :car-model]}
:validators
[(fn [v]
(when (empty? v)
"Enter car model name"))
(fn [v]
(when-not (re-matches #"(suzuki|mazda|honda|hyundai).*" v)
(str "Enter model for suzuki, mazda, honda, hyundai: " v)))
]}
:validation-car-specs
{:commando/mutation :ui/validate
:value {:commando/from [:state :car-specs]}
:validators [(fn [v]
(when (empty? v) "Enter car specs"))]}
:validated?
{:commando/fn #(not (boolean (not-empty (concat %1 %2))))
:args [{:commando/from [:validation-car-model]}
{:commando/from [:validation-car-specs]}]}
:on-change-event
{:commando/fn (fn [validated? data]
(when validated?
(js/console.log data)))
:args [{:commando/from [:validated?]}
{:commando/from [:state]}]}})))
get-instruction-value (fn [kvs] (reagent.core/track (fn [] (get-in @state-instruction (into [:instruction] kvs)))))
set-value (fn [k v] (swap! state assoc k v))]
(fn []
[:div
[:div
"Car model"
[:input {:defaultValue (:car-model @state)
:on-change #(set-value :car-model (.. % -target -value))}]]
(for [error-string @(get-instruction-value [:validation-car-model])]
[:span {:style {:color "darkred" :margin-left "10px"}} error-string])
[:div
"Car Specs"
[:input {:defaultValue (:car-specs @state)
:on-change #(set-value :car-specs (.. % -target -value))}]]
(for [error-string @(get-instruction-value [:validation-car-specs])]
[:span {:style {:color "darkred" :margin-left "10px"}} error-string])
[:input {:disabled (not @(get-instruction-value [:validated?]))
:type "button"
:on-click #(js/window.alert (js/JSON.stringify (clj->js @(get-instruction-value [:state]))))
:value "submit"}]
[:pre
(with-out-str
(cljs.pprint/pprint
(:instruction @state-instruction)))]
])))
(defmethod commands-builtin/command-mutation :ui/validate [_ {:keys [value validators]}]
;; The validator returns a list of error messages or nil.
(not-empty
(reduce
(fn [acc validator]
(if-let [msg (validator value)]
(conj acc msg)
acc))
[]
validators)))
All dependencies (validation results, field values, etc.) are described as data, making it easy to compose and trace. Command results (like :validated?
) drive UI state directly (e.g., disabling the submit button).
The instruction is a fully declarative pipeline. Changing business logic or validation rules. Is a matter of editing data, not code.
:validated?
) directly controls UI state.In the pattern above, every state change triggers a full re-execution of the instruction, including parsing, dependency analysis, and command evaluation. This is fine for small forms, but for larger instructions or frequent updates, it can be inefficient.
Commando provides a compilation mechanism: If your instruction structure is stable, you can "compile" it once, then re-execute only the final evaluation step as state changes.
(def form-instruction-compiler
(commando/build-compiler
[commands-builtin/command-mutation-spec
commands-builtin/command-from-spec
commands-builtin/command-fn-spec]
{:state {:car-model nil :car-specs nil}
:validation-car-model
{:commando/mutation :ui/validate
:value {:commando/from [:state :car-model]}
:validators ...}
...}))
You must provide default values for all fields referenced by :commando/from
. Otherwise, execution will fail if a key command pointing is missing(i.e. :commando/from [:state :car-model]
need to be founded even if the value is nil)
Now, in your component, you only need to update the state and re-execute the compiled instruction:
(defn form-component []
(let [state (reagent.core/atom {:car-model ""
:car-specs ""})
state-instruction
(reagent.core/track
(fn []
(commando/execute
form-instruction-compiler
{:state @state
:validation-car-model
{:commando/mutation :ui/validate
:value {:commando/from [:state :car-model]}
:validators ...}
...})))]
(fn []
[:div ...])))
Result: Evaluation time is now much faster(because it practicaly linear) for large instruction graphs.
commando/execute
can have :status :failed
. This indicates a problem with the instruction structure, not a user or business error. UI code should not directly react to these errors, just as you wouldn't use exceptions for routine validation.Comment: Use validation results inside the instruction to drive UI, not the error status of Commando itself.
Commando allows you to manage all form logic, validation, and state transformation declaratively, making your UI code simpler, more maintainable, and easier to reason about.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
Ctrl+k | Jump to recent docs |
← | Move to previous article |
→ | Move to next article |
Ctrl+/ | Jump to the search field |