Purpose: Understanding RAD's attribute-driven approach - the "why" behind forms and reports. Scope: RAD layer only. See FULCRO.md for core concepts.
RAD is attribute-driven development. Define your data schema once as attributes, then generate forms and reports from them.
┌─────────────────────────────────────────────────────────────┐
│ Attributes │
│ "person/name is a required string on the person entity" │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐
│ Forms │ │ Reports │ │ Resolvers│
│ (CRUD) │ │ (Lists) │ │ (Data) │
└─────────┘ └──────────┘ └──────────┘
Key insight: Attributes are the single source of truth. Forms, reports, and resolvers derive behavior from them.
Every piece of data is defined as an attribute:
(defattr person_name :person/name :string
{ao/identities #{:person/id} ; Which entity this belongs to
ao/required? true}) ; Validation ruleEvery entity needs exactly one identity (its primary key):
(defattr person_id :person/id :string
{ao/identity? true}) ; Makes this the primary keyLink entities together (foreign keys):
(defattr person_homeworld :person/homeworld :ref
{ao/identities #{:person/id}
ao/target :planet/id ; What it points to
ao/cardinality :one}) ; :one or :manyEntry points for reports:
(defattr all-people :swapi/all-people :ref
{ao/target :person/id
ao/pc-resolve :swapi/all-people}) ; Resolver that provides dataForms are state machines for editing entities. They handle dirty tracking, validation, save/cancel automatically.
(form/defsc-form PersonForm [this props]
{fo/id person_id ; Identity attribute
fo/attributes [person_name ; Fields to show
person_height]
fo/read-only? true ; View-only mode
fo/route-prefix "person"}) ; URL: /person/:id:initial → :editing → :saving → :saved
↑ │
└───────────┘ (on error)
RAD tracks dirty fields, validates on save, and handles optimistic updates.
For selecting related entities:
fo/field-styles {:person/homeworld :pick-one}
fo/field-options {:person/homeworld
{po/query-key :swapi/all-planets
po/query [:planet/id :planet/name]
po/options-xform (fn [_ planets]
(mapv #(hash-map
:text (:planet/name %)
:value [:planet/id (:planet/id %)])
planets))}}Critical: options-xform must return [{:text "..." :value [:id-key id]}] format.
Reports are state machines for data tables. They handle loading, filtering, sorting, pagination.
(report/defsc-report PersonList [this props]
{ro/title "All People"
ro/source-attribute all-people ; Collection attribute
ro/row-pk :person/id ; Unique row identifier
ro/columns [:person/name ; Columns to display
:person/height]
ro/route "people"}) ; URL: /people:initial → :loading → :loaded → :ready
│
▼
(filter/sort/paginate)
Reports can have controls (search boxes, filters):
ro/controls {::search-term {:type :string :label "Search"}}
ro/load-options (fn [env]
(let [params (report/current-control-parameters env)]
{:params {:search-term (::search-term params)}}))The resolver receives these in :query-params:
(pco/defresolver all-people [{:keys [query-params]} _]
(let [term (:search-term query-params)]
...))RAD forms and reports use Pathom resolvers for data:
| RAD Component | Pathom Usage |
|---|---|
| Form load | Entity resolver by ident [:person/id "1"] |
| Form save | Mutation |
| Report load | Root resolver via ro/source-attribute |
The ao/pc-resolve on collection attributes tells RAD which resolver provides the data.
Wrong - returning raw data:
po/options-xform (fn [_ opts] opts) ; ❌Right - transform to required format:
po/options-xform (fn [_ opts]
(mapv #(hash-map :text (:name %)
:value [:thing/id (:id %)])
opts)) ; ✅If report shows [{} {} {}], the resolver output shape doesn't match what RAD expects. Check:
ro/row-pkmatches the ID field in resultsro/columnsare fields the resolver actually provides
Same as core Fulcro - use keyword shorthand:
:ident :person/id ; ✅ Not (fn [] ...)Attributes must be in model_rad/attributes.cljc:
(def all-attributes (concat api1/attributes api2/attributes ...))(require '[us.whitford.fulcro-radar.api :as radar])
(def p (radar/get-parser))
;; All forms with their attributes
(->> (p {} [:radar/overview]) :radar/overview :radar/forms
(map #(select-keys % [:name :route :id-key :attributes])))
;; All reports with their sources
(->> (p {} [:radar/overview]) :radar/overview :radar/reports
(map #(select-keys % [:name :route :source :columns])))