# Component cookbook GROWS every migration. Each entry: what it is, the swap rule it uses, and the canonical snippet. Reuse these before writing anything new; the success signal is *more reuse each migration*. Seeded from `transaction/edit.clj` (Hiccup form — Selmer versions land in Phase 2). --- ## typeahead (account / vendor) — Alpine + tippy, survives swaps Used for account and vendor selection. Click-to-select (not a live text caret), so a whole-form swap on change is safe. Null-guard `tippy?`/`$refs.input?`. ```clojure (defn account-typeahead* [{:keys [name value client-id x-model]}] [:div.flex.flex-col (com/typeahead {:name name :placeholder "Search..." :url (hu/url (bidi/path-for ssr-routes/only-routes :account-search) (cond-> {:purpose "transaction"} client-id (assoc :client-id client-id))) :id name :x-model x-model ; binds selected value into the row's Alpine scope :value value :content-fn (fn [v] (:account/name (d-accounts/clientize ... v client-id)))})]) ``` Reuse note: `:x-model` lets the *parent* row read the selected id (e.g. `accountId`) to gate a targeted location swap. See account-row. ## account-row — cursor render fn + per-row targeted location swap + whole-form remove The canonical "row in a repeated grid" pattern. One render fn, top-rooted cursor. - account typeahead binds `accountId` into row Alpine scope; - **location cell** swaps *only itself* (`#account-location-`) on `changed` (swap-doctrine Rule 2); - **amount cell** swaps *only* `#account-totals` (Rule 4, sibling tbody); - **remove** swaps the whole form (Rule 3). ```clojure (defn transaction-account-row* [{:keys [value client-id amount-mode index]}] (com/data-grid-row (-> {:class "account-row" :id (str "account-row-" index) :x-data (hx/json {:show ... :accountId (fc/field-value (:transaction-account/account value))}) :data-key "show" :x-ref "p"} hx/alpine-mount-then-appear) (fc/with-field :db/id (com/hidden {:name (fc/field-name) :value (fc/field-value)})) (fc/with-field :transaction-account/account (com/data-grid-cell {} (com/validated-field {:errors (fc/field-errors)} (account-typeahead* {:value (fc/field-value) :client-id client-id :name (fc/field-name) :x-model "accountId"})))) (fc/with-field :transaction-account/location (com/data-grid-cell {:id (str "account-location-" index)} ...Rule 2 targeted swap...)) (fc/with-field :transaction-account/amount (com/data-grid-cell {} ...Rule 4 totals swap...)) (com/data-grid-cell {:class "align-top"} ...Rule 3 whole-form remove...))) ``` TODO Phase 2: drop the `transaction-account-row-no-cursor*` twin; this is the only kept form. ## totals in a sibling `` — Rule 4 instead of OOB Running totals live in their own ``, a sibling of the input-bearing rows, so an amount edit refreshes them with a plain targeted swap and never replaces the amount input (caret survives). ```clojure (com/data-grid {:footer-tbody [:tbody {:id "account-totals"} (com/data-grid-row {:class "account-total-row"} ... (account-total* request) ...) (com/data-grid-row {:class "account-balance-row"} ... (account-balance* request) ...)]} ...input rows...) ``` ## money-input / text-input amount field — Rule 4 targeted totals swap ```clojure (com/money-input {:name (fc/field-name) :id (str "account-amount-" index) :class "w-16 account-amount-field" :hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed) :hx-target "#account-totals" :hx-select "#account-totals" :hx-swap "outerHTML" :hx-trigger "keyup changed delay:300ms" :hx-include "closest form"}) ``` `%` mode swaps to `com/text-input {:type "number" :step "0.01"}` with the same swap attrs. ## memo field — Rule 1, no request ```clojure (com/text-input {:value (fc/field-value) :name (fc/field-name) :id "edit-memo" :placeholder "Optional note"}) ; no hx-* — rides along to save ``` ## mode toggle ($/% radio, simple/advanced link) — Rule 3, whole-form swap ```clojure (com/radio-card {:options [{:value "$" :content "$"} {:value "%" :content "%"}] :value amount-mode :name "step-params[amount-mode]" :hx-post (bidi/path-for ssr-routes/only-routes ::route/toggle-amount-mode) :hx-target "#wizard-form" :hx-select "#wizard-form" :hx-swap "outerHTML" :hx-include "closest form"}) ``` TODO Phase 2: the simple/advanced toggle becomes a `?mode=` re-render (plain form), not a dedicated route.