refactor(ssr): revert hiccup→Selmer migration; render forms in Hiccup again
Abandons the Selmer-templating step of the SSR re-authoring and moves the four migrated form/wizard modals back to Hiccup (com/* components), keeping the whole-form HTMX swap doctrine, top-rooted render functions, and the session-backed wizard engine unchanged. - transaction/edit, transaction/bulk_code, invoices (bulk-edit group), and pos/sales_summaries render via com/* again; every hx-* swap (whole-form + targeted location-cell / totals-tbody / inline account-cell swaps) is preserved exactly. - add com/single-modal-card to centralize the md:w-[950px] md:h-[650px] modal chrome that previously lived only in the Selmer modal-card templates. - delete auto-ap.ssr.selmer, auto-ap.ssr.components.selmer, selmer_test, the whole resources/templates tree (55 files), the selmer dependency, and the tailwind resources/templates content glob. - strip Selmer guidance from the ssr-form-migration skill + modernization plan. Verified: all four namespaces compile and render with no stringified-hiccup leaks; output.css rebuilds byte-identically (no Tailwind class loss); 60 e2e specs pass — the four reverted modals (incl. whole-form-swap focus/caret tests) plus the untouched wizard/pay/new/rule modals. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: ssr-form-migration
|
||||
description: Migrate an SSR form or wizard modal to the whole-form HTMX swap doctrine, top-rooted render functions, the data-driven session-backed wizard engine, and (where it helps) Selmer templates. Use when simplifying a server-rendered form/wizard modal in this codebase, removing EDN-snapshot round-trips, deleting *-no-cursor* duplicate render fns, collapsing per-interaction routes, or replacing the multi-step wizard protocol machinery.
|
||||
description: Migrate an SSR form or wizard modal to the whole-form HTMX swap doctrine, top-rooted render functions, and the data-driven session-backed wizard engine. Use when simplifying a server-rendered form/wizard modal in this codebase, removing EDN-snapshot round-trips, deleting *-no-cursor* duplicate render fns, collapsing per-interaction routes, or replacing the multi-step wizard protocol machinery. Rendering stays in Hiccup (`com/*` components) — the earlier Selmer-templating step was abandoned.
|
||||
---
|
||||
|
||||
# SSR Form & Wizard Migration
|
||||
@@ -12,7 +12,7 @@ approach with **zero out-of-band swaps**. Every migration *reads this skill firs
|
||||
*extends it last* (the Growth contract below). If migration N+1 is not easier than N,
|
||||
the skill-update step was skipped — treat that as a bug.
|
||||
|
||||
The four patterns every migration moves code toward live in `reference/`:
|
||||
The three patterns every migration moves code toward live in `reference/`:
|
||||
|
||||
- `reference/swap-doctrine.md` — the four-rule HTMX swap priority order + the focus
|
||||
invariant + Alpine-survives-swap hardening + target-selector strategy.
|
||||
@@ -20,8 +20,11 @@ The four patterns every migration moves code toward live in `reference/`:
|
||||
**or a top-rooted cursor**; no faked cursor positions, no `*-no-cursor*` twins.
|
||||
- `reference/form-vs-wizard.md` — single-step → plain form; multi-step → data-driven
|
||||
engine with **per-step state in the Ring session** (the Django `formtools` model).
|
||||
- `reference/selmer-conventions.md` — plain-HTML attributes via Selmer, the
|
||||
Hiccup↔Selmer interop bridge, include/block patterns.
|
||||
|
||||
> **Rendering stays in Hiccup.** An earlier iteration of this skill added a fourth
|
||||
> pattern — templating interactive components in Selmer — which was later **abandoned**.
|
||||
> All modals render through the shared Hiccup components (`com/*`); there is no Selmer
|
||||
> layer. Ignore any residual Selmer references in the cookbooks below.
|
||||
|
||||
Growing cookbooks (append every migration):
|
||||
`component-cookbook.md`, `gotchas.md`, `test-recipes.md`, `scorecard.md`.
|
||||
@@ -65,9 +68,9 @@ Run this loop for each modal. The phase notes in the migration plan list only wh
|
||||
and any `with-cursor`/`MapCursor` rebind that fakes a deep starting position
|
||||
(heuristics 1, 2). Using a cursor is fine; faking where it *starts* is not.
|
||||
|
||||
6. **Templatize in Selmer** (`reference/selmer-conventions.md`) where the component is
|
||||
interactive/attribute-heavy. Reuse cookbook bits; add new ones back (heuristics 5, 8).
|
||||
Static markup may stay Hiccup — Selmer scope is hybrid (Open decision 2).
|
||||
6. **Render in Hiccup** with the shared `com/*` components. Reuse cookbook bits; add new
|
||||
ones back (heuristic 5). (An earlier version of this step templated interactive
|
||||
components in Selmer; that was abandoned — everything renders through Hiccup.)
|
||||
|
||||
7. **Wire HTMX per the swap doctrine** (`reference/swap-doctrine.md`). Keep the focus
|
||||
invariant intact. No OOB except a genuinely disjoint region, documented (heuristic 7).
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Component cookbook
|
||||
|
||||
> **Note:** Selmer was abandoned — all rendering is Hiccup (`com/*`). Ignore any
|
||||
> Selmer/template/`sc/*` references below; use the equivalent `com/*` Hiccup component.
|
||||
|
||||
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*.
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Quality scorecard (the ratchet)
|
||||
|
||||
> **Note:** Selmer was abandoned — rendering is Hiccup (`com/*`). Ignore the Selmer-specific
|
||||
> heuristics/mentions below; the swap-doctrine, render-function, and engine ratchets still apply.
|
||||
|
||||
Cheap to measure (`grep -c`, `wc -l`, `clj-kondo`), recorded **before/after each
|
||||
migration** in the commit message and in the results table below. **No metric may regress
|
||||
for the touched modal** without a written exception in `gotchas.md`. These are directional
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
# Selmer template conventions
|
||||
|
||||
> **Validated** in the Transaction Edit migration: `location-select*` now renders from
|
||||
> `resources/templates/components/location-select.html` via the interop bridge, embedded
|
||||
> back into the Hiccup account row. Verified: swap spec 6/6, transaction-edit 8/8 (the
|
||||
> Shared Location test selects through the Selmer `<select>`, saves, and spreads to DT).
|
||||
|
||||
## Why Selmer for interactive components
|
||||
|
||||
In Hiccup the same Alpine/HTMX attribute is sometimes a keyword and sometimes a string in
|
||||
the same file — there's no rule a reader (or an LLM) can rely on. The real
|
||||
`com/typeahead-` mixes them in one map:
|
||||
|
||||
```clojure
|
||||
:x-modelable "value.value" ; keyword key
|
||||
"x-ref" "hidden" ; string key
|
||||
"@keydown.down.prevent.stop" "tippy?.show();" ; handlers MUST be strings
|
||||
:x-init "..." ; structural attrs are keywords
|
||||
```
|
||||
|
||||
In a Selmer template the same markup is unambiguous plain HTML:
|
||||
|
||||
```html
|
||||
{# templates/components/typeahead.html #}
|
||||
<div class="relative" x-data="{{ x_data|safe }}" x-model="{{ x_model }}">
|
||||
<a class="{{ classes }}" x-ref="input" tabindex="0"
|
||||
@keydown.down.prevent.stop="tippy?.show()"
|
||||
@keydown.backspace="tippy?.hide(); value = {value:'', label:''}">
|
||||
<span x-text="value.label"></span>
|
||||
</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
Note the `tippy?.` null-guard carried over from the swap doctrine — Selmer doesn't change
|
||||
the Alpine-survives-swap requirement.
|
||||
|
||||
## The render helper + interop bridge (`auto-ap.ssr.selmer`)
|
||||
|
||||
```clojure
|
||||
(sel/render template ctx) ; selmer/render-file -> HTML string (classpath-relative path)
|
||||
(sel/render-str template ctx) ; render from a string (tests/REPL)
|
||||
(sel/hiccup->html h) ; Hiccup -> string, for {{ frag|safe }} inside a template
|
||||
(sel/raw html-string) ; wrap a rendered string so hiccup2 emits it verbatim
|
||||
(sel/render->hiccup template ctx); render + raw, ready to drop into a Hiccup tree
|
||||
```
|
||||
|
||||
The bridge works **both ways** (proven in `selmer_test`): a Hiccup component renders inside
|
||||
a Selmer template (`hiccup->html` + `|safe`), and a Selmer fragment renders inside a Hiccup
|
||||
tree (`render->hiccup`, which `raw`-wraps so hiccup2 doesn't double-escape).
|
||||
|
||||
## The worked example — `location-select*`
|
||||
|
||||
Template (`resources/templates/components/location-select.html`): plain HTML, an
|
||||
`{% for %}` over option maps, `{% if opt.selected %}`.
|
||||
|
||||
```clojure
|
||||
;; Clojure side: build the data, compute classes (reuse inputs/default-input-classes so
|
||||
;; styling can't drift), render, and return a Hiccup-embeddable fragment.
|
||||
(defn location-select* [{:keys [name client-locations value ...]}]
|
||||
(let [options (cond ...) ; [[value label] ...]
|
||||
selected (or value (ffirst options))
|
||||
classes (str/join " " (conj (vec inputs/default-input-classes) "w-full"))]
|
||||
(sel/render->hiccup "templates/components/location-select.html"
|
||||
{:name name :classes classes
|
||||
:options (for [[v l] options] {:value v :label l :selected (= v selected)})})))
|
||||
```
|
||||
|
||||
Lessons:
|
||||
- **Pass computed values in, don't hard-code.** Reuse the Clojure source of truth
|
||||
(`inputs/default-input-classes`) as a context value rather than copying class strings
|
||||
into the template — otherwise styling drifts from the shared components.
|
||||
- **Verify by string-match + e2e, not byte-parity.** `hh/add-class` is set-based, so class
|
||||
*order* differs from the old `com/select` output; CSS is order-independent and the e2e
|
||||
proves behavior. (`testing-conventions`: don't assert on exact markup.)
|
||||
- **Embed via the bridge.** `render->hiccup` lets the Selmer fragment sit inside the
|
||||
still-Hiccup row (`com/validated-field` wraps it) — strangler, one component at a time.
|
||||
|
||||
## Composition — verified mechanics (selmer 1.12.61)
|
||||
|
||||
Proven by REPL before the full migration (do the same before relying on any of these):
|
||||
|
||||
- **`{% include %}` works in FILE renders only.** `sel/render` = `selmer/render-file`, and
|
||||
include/extends/block are *parse-stage* tags. Rendering a template **string** that
|
||||
contains `{% include %}` throws `unrecognized tag: :include` (the runtime expr-tag has a
|
||||
nil handler). So includes only work from a `.html` file, never from `render-str`.
|
||||
- **`{% include %}` sees `{% for %}` loop bindings.** Inside `{% for row in rows %}…{% include "components/x.html" %}…{% endfor %}` the partial can read `row.*`. Good for repeated
|
||||
rows — though Clojure-composing the rows (below) is usually simpler.
|
||||
- **`{% include … with k=v %}` is IGNORED** in 1.12.61 (the `with` clause is dropped). To
|
||||
parametrize, either wrap with the block tag `{% with k=v %}{% include … %}{% endwith %}`
|
||||
(works), or — preferred — let a Clojure wrapper render the partial with an explicit ctx.
|
||||
|
||||
## The Clojure-wrapper composition pattern (`auto-ap.ssr.components.selmer` / `sc`)
|
||||
|
||||
Because `{% include with %}` can't pass args and the server computes most values anyway,
|
||||
each shared component is a **thin Clojure wrapper that renders its own partial** (the
|
||||
proven `location-select*` shape, generalised). The element *structure* lives 100% in the
|
||||
`.html`; the only Clojure is data assembly. The modal's render fns compose these wrappers
|
||||
and assemble a view-model, interpolating the pre-rendered fragments as `{{ frag|safe }}`.
|
||||
|
||||
```clojure
|
||||
(sc/hidden {:name … :value …}) ; -> render "components/hidden.html"
|
||||
(sc/validated-field {:label … :errors …} body…)
|
||||
(sc/typeahead {:name … :url … :value … :content-fn …}) ; resolves label server-side
|
||||
(sc/data-grid {:headers […] :footer-tbody …} rows…)
|
||||
```
|
||||
|
||||
### `attrs->str` — the dynamic-attribute bridge
|
||||
|
||||
HTMX/Alpine attributes vary per call site, so a fixed template can't enumerate them.
|
||||
`sc/attrs->str` serialises an attribute *map* to an HTML attribute *string* injected with
|
||||
`{{ attrs|safe }}`: `<input type="hidden"{{ attrs|safe }}>`. Rules mirror hiccup2 — nil/false
|
||||
dropped, `true` → bare boolean attr, else `name="escaped"` (via `hu/escape-html`, so JSON
|
||||
`x-data` and `x-init` quotes become `"`/`'` and the browser decodes them back).
|
||||
Keyword keys keep their full name incl. namespaced/colon forms (`:x-hx-val:account-id` →
|
||||
`x-hx-val:account-id`). This keeps templates free of per-attribute `{% if %}` ladders while
|
||||
still emitting 100% Selmer markup — `attrs->str` serialises data, it does not build Hiccup.
|
||||
|
||||
### Reuse the real class helpers
|
||||
|
||||
Wrappers reuse `inputs/default-input-classes`, `inputs/use-size`, `hh/add-class`,
|
||||
`btn/bg-colors` so output matches the Hiccup component **modulo Tailwind class order**.
|
||||
Verify by class-**set** equality + e2e, never byte-parity (see the cookbook entries).
|
||||
|
||||
### Trivial wrapper divs
|
||||
|
||||
A bare `<div class="w-72">…</div>` around a fragment is composed with a `wrap-div` string
|
||||
helper (or put the class in the parent template), not a Hiccup vector — string composition
|
||||
of a structural wrapper is not Hiccup and avoids a micro-template per div.
|
||||
|
||||
Keep `|safe` to values the server fully controls (rendered fragments, JSON for `x-data`),
|
||||
never raw user input.
|
||||
|
||||
## Scope (Open decision 2)
|
||||
|
||||
Hybrid, and the boundary is real: **the modal's attribute-heavy components delegate to the
|
||||
shared `com/typeahead` / `com/select` / `com/button-group-button`.** Converting those is a
|
||||
*cross-cutting* change (every modal uses them), so it belongs to the Phase 11 Selmer sweep,
|
||||
not a single modal. `location-select*` is the first, self-contained proof; the shared
|
||||
components follow when the sweep promotes them to Selmer partials.
|
||||
|
||||
## Attribute-consistency scorecard (heuristic 8)
|
||||
|
||||
```bash
|
||||
grep -cE '"x-[a-z]|"hx-[a-z]|"@' <migrated-template> # → 0 mixed encodings in Selmer
|
||||
```
|
||||
A migrated Selmer template has no mixed `:x-`/`"x-"` encodings because everything is plain
|
||||
HTML. (The Hiccup `"@click"`/`":class"` offenders that remain in `edit.clj` live in the
|
||||
shared-component call sites — they clear when those components move to Selmer.)
|
||||
Reference in New Issue
Block a user