shared-ui
com.wagoe/wagoe-shared-ui holds the Hiccup building blocks that more than one
library renders with. It was extracted so admin and user could stop
depending on each other’s UI code — that mutual dependency was a genuine cycle
until this library broke it.
What it is
A library of pure functions returning Hiccup. No I/O, no state, no ports.
That last part is deliberate and worth stating, because it makes shared-ui the
one exception to Wagoe’s usual public-API rule. Elsewhere core/ is an
implementation detail and you depend on ports.clj; here there is no port,
because there is nothing to abstract over — a function that turns arguments into
a vector has no adapter to swap. The functions under wagoe.shared.ui.core.
*are the public surface, and the stability policy
covers them as such.
Namespaces
| Namespace | Contains |
|---|---|
|
Form controls and inline feedback: |
|
Page scaffolding: |
|
The admin shell (sidebar, topbar) and the signed-in user’s menu |
|
|
|
|
|
Typeahead input that asks the server for options |
|
Form attributes, decoding and errors from a Malli map schema |
|
|
|
Turning Malli explain output into per-field messages: |
|
Inline SVG icon set |
|
Alpine.js attribute helpers for HTMX-driven interactions |
Relationship to ui-style
shared-ui produces markup; ui-style ships the CSS and
JS that markup expects. The class names the components emit are the contract
between them, which is why the two are versioned in lockstep like everything
else.
Usage
(require '[wagoe.shared.ui.core.components :as ui]
'[wagoe.shared.ui.core.layout :as layout])
(layout/page-layout
"Products"
[:div
(ui/text-input :name "" {:placeholder "Product name"})
(ui/submit-button "Save")])
Because every function is pure, they are straightforward to test — call one and assert on the Hiccup vector it returns, with no rendering step and no system to start.
Tables
The table’s controls request the list’s own URL and push it, so sort, filters and page live in the address bar. The handler answers an htmx request with the table and anything else with the whole page.
(require '[wagoe.shared.ui.core.table :as table])
(defn orders-table [orders tq filters total]
[:div#orders
(table/data-table
{:url "/orders"
:hx-target "#orders"
:table-query tq
:filters filters
:columns [{:field :number :label "Number"}
{:field :status :label "Status" :filter {:options [["paid" "Paid"]]}}]
:rows (for [o orders] [:tr [:td (:number o)] [:td (:status o)]])})
(table/pagination {:table-query tq :total-count total
:base-url "/orders" :hx-target "#orders" :extra-params filters})])
;; In the handler
(let [{:keys [filters] :as tq} (table/parse-table-params (:query-params request)
{:sortable #{:number :status}
:max-page-size 100})]
...)
With :infinite? true (and :total-count and :rows-url) the last row loads the next page
once it scrolls into view; the handler at :rows-url answers with just the
<tr> rows. Send Vary: HX-Request, HX-Target from a handler that answers one URL with
both the page and the fragment.