Squashed Phase-2 SSR work: migrate the Transaction Edit modal's render path entirely to Selmer templates (zero Hiccup in the render path), rip out the multi-step wizard abstraction (EditWizard/LinksStep records, MultiStepFormState, step-params[...] field names, mm/* middleware) in favor of a plain form with flat derived state, and promote shared UI components to reusable Selmer partials under resources/templates/components/. Adds the Selmer interop bridge, the auto-ap.ssr.components.selmer (sc) wrapper library, and the ssr-form-migration skill capturing the learnings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
123 lines
6.4 KiB
Markdown
123 lines
6.4 KiB
Markdown
---
|
|
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.
|
|
---
|
|
|
|
# SSR Form & Wizard Migration
|
|
|
|
A repeatable method for making a server-rendered form/wizard modal **simpler** without
|
|
changing user-facing behavior. Distilled from the first proven migration — the
|
|
`transaction/edit.clj` modal, which already runs on the whole-form `hx-select` swap
|
|
approach with **zero out-of-band swaps**. Every migration *reads this skill first* and
|
|
*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/`:
|
|
|
|
- `reference/swap-doctrine.md` — the four-rule HTMX swap priority order + the focus
|
|
invariant + Alpine-survives-swap hardening + target-selector strategy.
|
|
- `reference/render-functions.md` — one render fn per component, taking explicit data
|
|
**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.
|
|
|
|
Growing cookbooks (append every migration):
|
|
`component-cookbook.md`, `gotchas.md`, `test-recipes.md`, `scorecard.md`.
|
|
|
|
---
|
|
|
|
## The per-migration playbook
|
|
|
|
Run this loop for each modal. The phase notes in the migration plan list only what is
|
|
*specific* to a modal; this loop is the constant.
|
|
|
|
1. **Read the skill.** Skim `reference/` and note which `component-cookbook.md`
|
|
entries and `gotchas.md` you can reuse. Start from the cookbook, not a blank file.
|
|
|
|
2. **Classify** (`reference/form-vs-wizard.md`).
|
|
- Single logical step (even with a `?mode=` toggle or add/remove rows) → **plain
|
|
form**: no server-side wizard state, no snapshot, no protocol.
|
|
- Genuinely multiple steps the user advances through → **wizard**: the data-driven
|
|
engine + per-step session storage.
|
|
- When in doubt, it's a form.
|
|
|
|
3. **Baseline the scorecard** (`scorecard.md`, heuristics in §6 of the plan). Record
|
|
before-numbers with cheap tools:
|
|
```bash
|
|
F=src/clj/auto_ap/ssr/<modal>.clj
|
|
wc -l $F # LOC (heuristic 4)
|
|
grep -c 'defn.*-no-cursor' $F # *-no-cursor* twins (heuristic 1)
|
|
grep -cE 'with-cursor|MapCursor\.' $F # faked cursor re-roots (heuristic 1)
|
|
grep -c 'hx-swap-oob' $F # OOB swaps (heuristic 7)
|
|
grep -cE '"hx-[a-z]' $F # mixed string hx- attrs (heuristic 8)
|
|
# route count: count this modal's entries in src/cljc/auto_ap/routes/*.cljc
|
|
```
|
|
|
|
4. **Characterize behavior (test-first).** Write or confirm a Playwright spec that
|
|
captures *current* behavior before you touch anything — focus/caret survival across
|
|
swaps, each field round-trip, validation errors, and the real save. This spec is the
|
|
parity contract; it must stay green through every commit. See `test-recipes.md`.
|
|
|
|
5. **Consolidate render functions** (`reference/render-functions.md`). Make each render
|
|
fn take explicit data or a **top-rooted cursor**. Delete `*-no-cursor*` duplicates
|
|
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).
|
|
|
|
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).
|
|
|
|
8. **Collapse routes** to 2 (`GET` open, `POST` submit) — `+1` for an add-row endpoint,
|
|
`+1` for the single `*-form-changed` whole-form re-render endpoint (heuristic 6).
|
|
|
|
9. **Verify.** Modal e2e green + full e2e suite at-or-above baseline; assert DB
|
|
mutations by querying Datomic, not markup; REPL-check the pure render/data fns.
|
|
Re-measure the scorecard — **no metric may regress for the touched modal** without a
|
|
written exception in `gotchas.md`.
|
|
|
|
10. **Commit** one reversible feature commit. The message includes the scorecard delta
|
|
and the reused/new cookbook entries.
|
|
|
|
11. **Feed the skill** (the Growth contract). *Not optional.*
|
|
|
|
---
|
|
|
|
## Growth contract — the last task of every migration
|
|
|
|
- Converted a component? → add its before/after to `component-cookbook.md`.
|
|
- Hit a surprise? → one entry in `gotchas.md`.
|
|
- Found a test pattern? → `test-recipes.md`.
|
|
- Playbook step missing or wrong? → fix this `SKILL.md`.
|
|
- Measured the scorecard? → append the row to `scorecard.md`.
|
|
|
|
**Success signal:** each migration reuses more cookbook entries and starts from a better
|
|
scorecard baseline than the previous one.
|
|
|
|
---
|
|
|
|
## Non-negotiables
|
|
|
|
- **Focus invariant:** the input the user is typing in is *never* inside the region its
|
|
own request swaps. Violating this drops the caret. (Proven by the
|
|
`transaction-edit-swap.spec.ts` caret tests.)
|
|
- **No new OOB swaps.** If tempted to OOB something inside the same feature, restructure
|
|
the DOM so the dependent element shares an ancestor with the trigger and use an
|
|
ordinary swap (e.g. totals in a sibling `<tbody>`).
|
|
- **Behavior parity is proven by tests, not by reading.** The full e2e suite stays green
|
|
after every migration.
|
|
- **Don't game the heuristics.** They're directional evidence paired with the e2e parity
|
|
gate; review the trend, not single numbers.
|
|
|
|
## Project conventions that bite (see `gotchas.md`)
|
|
|
|
- Edit Clojure with the clojure-mcp tools (`clojure_edit`, `clojure_edit_replace_sexp`),
|
|
not the raw file editor. `clj-paren-repair` then `lein cljfmt fix` when a file won't
|
|
compile.
|
|
- Run tests via the `clojure-eval` skill / `clj-nrepl-eval -p PORT`, not `lein test`.
|
|
- Temp files go in `./tmp/`.
|