RAD forms support three types of dynamic behavior: computed UI fields (display-only calculations), derived stored fields (spreadsheet-style formulas that persist), and on-change triggers (field change handlers with side effects). These features enable interdependent calculations, cascading dropdowns, and complex form interactions.
From DevelopersGuide.adoc:1554-1558:
From DevelopersGuide.adoc:1560-1581:
Computed UI fields are read-only values calculated from other form data. They exist only during rendering and never appear in Fulcro state.
(ns com.example.model.line-item
(:require
[com.fulcrologic.rad.attributes :refer [defattr]]
[com.fulcrologic.rad.attributes-options :as ao]
[com.fulcrologic.rad.form-options :as fo]
[com.example.math :as math]))
(defattr subtotal :line-item/subtotal :decimal
{ao/computed-value (fn [{::fo/keys [props] :as form-env} attr]
(let [{:line-item/keys [quantity quoted-price]} props]
(math/round (math/* quantity quoted-price) 2)))})
ao/computed-value Signature: (fn [form-env attr] value)
Parameters:
form-env - The form rendering environment map, including ::fo/props (current form props)attr - The attribute definition of this computed fieldReturns:
Characteristics:
::fo/propsFrom DevelopersGuide.adoc:1579-1581:
"You actually have access to the entire set of props in the form, but you should note that other computed fields are not in the data model. So if you have data dependencies across computed fields you'll end up re-computing intermediate results."
Use Cases:
From DevelopersGuide.adoc:1582-1643:
Derived fields are calculated values that ARE stored in Fulcro state and can be saved to the database. They use the
fo/triggers mechanism with :derive-fields.
From form-options.cljc:270-276:
"*
:derive-fields- A(fn [props] new-props)that can rewrite any of the props on the form (as a tree). This function is allowed to look into subforms, and even generate new members (though it must be careful to add form config if it does so). Thenew-propsmust be a tree of props that matches the correct shape of the form and is non-destructive to the form config and other non-field attributes on that tree."
Signature: (fn [form-tree] updated-form-tree)
Parameters:
form-tree - Denormalized tree of form props (includes nested subforms)Returns:
Key Properties:
From DevelopersGuide.adoc:1589-1598:
(ns com.example.ui.line-item
(:require
[com.fulcrologic.rad.form :as form :refer [defsc-form]]
[com.fulcrologic.rad.form-options :as fo]
[com.example.model.line-item :as line-item]
[com.example.math :as math]))
(defn add-subtotal* [{:line-item/keys [quantity quoted-price] :as item}]
(assoc item :line-item/subtotal (math/* quantity quoted-price)))
(defsc-form LineItemForm [this props]
{fo/id line-item/id
fo/attributes [line-item/item line-item/quantity line-item/quoted-price line-item/subtotal]
fo/triggers {:derive-fields (fn [form-tree] (add-subtotal* form-tree))}})
From DevelopersGuide.adoc:1603-1609:
When both master and child forms have :derive-fields:
:derive-fields on the form where the attribute lives:derive-fields runs AFTER nested form's:derive-fieldsFrom DevelopersGuide.adoc:1610:
"Note: Deeply nested forms do not run
:derive-fieldsfor forms between the master and the form on which the attribute changed."
From DevelopersGuide.adoc:1616-1633:
(ns com.example.ui.invoice
(:require
[com.fulcrologic.rad.form :as form :refer [defsc-form]]
[com.fulcrologic.rad.form-options :as fo]
[com.example.model.invoice :as invoice]
[com.example.ui.line-item :refer [LineItemForm]]
[com.example.math :as math]))
(defn sum-subtotals* [{:invoice/keys [line-items] :as invoice}]
(assoc invoice :invoice/total
(reduce
(fn [t {:line-item/keys [subtotal]}]
(math/+ t subtotal))
(math/zero)
line-items)))
(defsc-form InvoiceForm [this props]
{fo/id invoice/id
fo/attributes [invoice/customer invoice/date invoice/line-items invoice/total]
fo/subforms {:invoice/line-items {fo/ui LineItemForm}}
fo/triggers {:derive-fields (fn [form-tree] (sum-subtotals* form-tree))}})
Flow:
quantity on a line itemLineItemForm's :derive-fields runs → updates :line-item/subtotalInvoiceForm's :derive-fields runs → updates :invoice/totalFrom DevelopersGuide.adoc:1637-1642:
"WARNING: It may be tempting to use this mechanism to invent values that are unrelated to the form and put them into the state. This is legal, but placing data in Fulcro's state database does not guarantee they will show up in rendered props."
If you add arbitrary keys to form state, they won't appear in props unless you:
fo/query-inclusionUse Cases:
From DevelopersGuide.adoc:1644-1700 and form-options.cljc:278-285:
On-change triggers handle user-driven field changes and enable side effects like loading data or conditionally updating fields.
From form-options.cljc:278-285:
"*
:on-change- Called when an individual field changes. A(fn [uism-env form-ident qualified-key old-value new-value] uism-env). The change handler has access to the UISM env ( which contains::uism/fulcro-appand::uism/state-map). This function is allowed to side-effect (trigger loads for dependent dropdowns, etc.). It must return the (optionally updated)uism-env."
Signature: (fn [uism-env form-ident k old-value new-value] uism-env-or-nil)
Parameters:
uism-env - Fulcro UI State Machine environment (contains ::uism/state-map, ::uism/fulcro-app)form-ident - Ident of the form being modified (e.g., [:line-item/id uuid])k - Qualified keyword of the attribute that changed (e.g., :line-item/item)old-value - Previous value of the attributenew-value - New value of the attributeReturns:
uism-env OR nil (nil means "do nothing")From DevelopersGuide.adoc:1669-1671:
"IMPORTANT: Handlers must either return an updated
envornil(which means "do nothing"). Returning anything else is an error."
From DevelopersGuide.adoc:1646-1655:
form/input-changed!):on-change won't trigger another :on-change):derive-fields triggersFrom DevelopersGuide.adoc:1699-1700:
"The
:on-changetriggers always precede:derive-fieldstriggers, so that the global derivation can depend upon values pushed from one field to another."
From DevelopersGuide.adoc:1657-1667:
(fn [env]
(-> env
(uism/apply-action ...)
(some-helper-you-wrote)
(cond->
condition? (optional-thing))))
Common UISM operations:
uism/apply-action - Update Fulcro state (like swap! but returns env)uism/trigger - Trigger loads, mutationsuism/store - Store data in the state machine's actor storageFrom DevelopersGuide.adoc:1675-1697:
(ns com.example.ui.line-item
(:require
[com.fulcrologic.fulcro.ui-state-machines :as uism]
[com.fulcrologic.rad.form :as form :refer [defsc-form]]
[com.fulcrologic.rad.form-options :as fo]
[com.example.model.line-item :as line-item]))
(defsc-form LineItemForm [this props]
{fo/id line-item/id
fo/attributes [line-item/item line-item/quantity line-item/quoted-price line-item/subtotal]
fo/triggers {:on-change (fn [{::uism/keys [state-map] :as uism-env}
form-ident k old-value new-value]
(case k
;; When item changes, auto-fill quoted-price from inventory
:line-item/item
(let [item-price (get-in state-map (conj new-value :item/price))
target-path (conj form-ident :line-item/quoted-price)]
;; apply-action works like (update state-map assoc-in ...)
(uism/apply-action uism-env assoc-in target-path item-price))
;; Default: do nothing
nil))
:derive-fields (fn [form-tree] (add-subtotal* form-tree))}})
Flow:
:on-change fires with new-value as item ident (e.g., [:item/id uuid]):item/price from normalized state:line-item/quoted-price:derive-fields fires → recalculates :line-item/subtotalFrom form-options.cljc:287-293:
Signature: (fn [uism-env ident] uism-env)
Called after form initialization (state machine started, but load may still be in progress).
fo/query-inclusion (only needed for new entities)Use Cases:
fo/default-valuesSignature: (fn [uism-env ident] uism-env)
Called after a successful save.
Use Cases:
Signature: (fn [uism-env ident] uism-env)
Called after a failed save.
Use Cases:
{fo/triggers
{:on-change (fn [{::uism/keys [fulcro-app] :as env} form-ident k old new]
(case k
:address/country
(-> env
;; Clear dependent field
(uism/apply-action assoc-in (conj form-ident :address/state) nil)
;; Load states for selected country
(uism/trigger :actor/form
`load-states
{:country new}))
nil))}}
(defn update-tax* [{:invoice/keys [subtotal customer-type tax-rate] :as invoice}]
(if (= :business customer-type)
(assoc invoice :invoice/tax (math/* subtotal tax-rate))
(assoc invoice :invoice/tax 0)))
{fo/triggers {:derive-fields update-tax*}}
{fo/triggers
{:on-change (fn [{::uism/keys [fulcro-app] :as env} form-ident k old new]
(case k
:account/username
(do
;; Side effect: trigger async validation mutation
(comp/transact! fulcro-app [(check-username-available {:username new})])
;; Return unchanged env (side effect only)
env)
nil))}}
(defn update-financials* [{:invoice/keys [subtotal tax-rate discount] :as invoice}]
(let [tax (math/* subtotal tax-rate)
total (math/- (math/+ subtotal tax) discount)]
(assoc invoice
:invoice/tax tax
:invoice/total total)))
{fo/triggers {:derive-fields update-financials*}}
{fo/triggers
{:started (fn [{::uism/keys [fulcro-app] :as env} form-ident]
(let [[_ id] form-ident
new? (tempid/tempid? id)]
(if new?
;; Load options needed for new forms
(-> env
(uism/trigger :actor/form `load-picker-options {:cache-key :all-customers}))
;; Existing form - options loaded via query inclusion
env)))}}
Understanding when triggers fire is critical:
form/input-changed! called:on-change trigger fires (on the form where field lives):derive-fields triggers fire:
Note: :on-change does NOT cascade. If you modify a field within :on-change, it won't trigger another
:on-change.
Computed Fields (ao/computed-value):
Derived Fields (:derive-fields):
From DevelopersGuide.adoc:1646-1649:
"The next dynamic support feature is the
:on-changetrigger. This trigger happens due to a user-driven change of an attribute on the form. Such triggers do not cascade."
If you update a field in :on-change, it won't trigger another :on-change. Use :derive-fields for cascading
updates.
From DevelopersGuide.adoc:1669-1671:
Returning anything other than an updated uism-env or nil is an error. The system will log console errors if you make
this mistake.
From DevelopersGuide.adoc:1603-1609:
:derive-fields runs first:derive-fields runs second (always):on-change handlers have access to the UISM environment and can:
uism/apply-actionuism/trigger::uism/fulcro-appNever use swap! or transact! directly within the handler function body. Use the UISM API to thread operations.
From DevelopersGuide.adoc:1637-1642:
If you add keys to form state that aren't defined as attributes in fo/attributes, you must also add them to
fo/query-inclusion. Otherwise, they won't appear in component props.
form/input-changed! and form lifecycle:on-change triggersao/computed-valuefo/triggers with all trigger types)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 |