Capture the proven whole-form hx-select swap method as a reusable skill so every later modal migration is cheaper and consistent. No app code changes. - SKILL.md: the per-migration playbook (classify → baseline → characterize → consolidate render fns → templatize → wire HTMX → collapse routes → verify → commit → feed skill) + Growth contract + non-negotiables. - reference/swap-doctrine.md: the four swap rules, focus invariant, Alpine-survives- swap hardening, target-selector strategy — worked from the real edit.clj swaps (memo no-request, account→location targeted cell, amount→totals sibling-tbody, vendor/mode/row whole-form). 0 OOB. - reference/render-functions.md: explicit-data or top-rooted cursor; the MapCursor fake + transaction-account-row-no-cursor* twin as the smell to remove. - reference/form-vs-wizard.md: classification + the data-driven session-backed (formtools SessionStorage) engine that replaces the snapshot round-trip + protocol. - reference/selmer-conventions.md: STUB, validated in Phase 2. - component-cookbook.md / gotchas.md / test-recipes.md / scorecard.md: seeded from what transaction-edit proves (7 cookbook entries, caret-survival + typeahead test recipes, scorecard baseline LOC 1608 / ~12 routes / 1 no-cursor twin / 2 faked roots / 0 OOB). Scorecard (Transaction Edit baseline, before Phase 2): LOC 1608, routes ~12, no-cursor twins 1, faked-cursor roots 2, snapshot merges ~75, OOB 0, mixed hx- 8.
56 lines
2.6 KiB
Markdown
56 lines
2.6 KiB
Markdown
# Gotchas
|
|
|
|
GROWS every migration. One entry per surprise. Also the home for any **written exception**
|
|
to the scorecard ratchet (a metric that regressed for a documented reason).
|
|
|
|
---
|
|
|
|
## Stale `$refs` / `tippy` after a swap
|
|
|
|
A whole-form swap can run an Alpine event handler *before* the component re-initialises,
|
|
so a handler that dereferences `$refs.input.__x_tippy` or calls `tippy.show()` throws.
|
|
**Always null-guard:** `$refs.input?.__x_tippy?.hide()`, `tippy?.show()`. The
|
|
`transaction-edit-swap.spec.ts` `trackErrors()` helper fails the test on any `pageerror`
|
|
or `console.error`, which is exactly how a stale-ref throw surfaces.
|
|
|
|
## Let the server value win — don't preserve Alpine state across a server-driven change
|
|
|
|
When a server change should update a component (e.g. choosing a vendor sets its default
|
|
account), rebuild that section fresh on the swap so the server-provided value lands
|
|
without keying tricks. The bug this prevents: "changing the vendor a *second* time doesn't
|
|
update the account" because preserved Alpine state shadowed the new server value. If you
|
|
*must* preserve a component, key it by value so a change forces re-init:
|
|
`(assoc attrs :key (str id "--" current-value))`.
|
|
|
|
## Focus dies if the typed input is inside its own swapped region
|
|
|
|
The single most important invariant. Amount field → swap a sibling tbody, not the row.
|
|
Memo → swap nothing. If a caret test (`sameNode`) fails, the input is in its own swap
|
|
region — re-target to a sibling/ancestor that excludes it.
|
|
|
|
## Faked cursors breed duplicate render fns
|
|
|
|
A `with-cursor`/`MapCursor` re-root to fake a deep start forces a `*-no-cursor*` twin.
|
|
Removing the fake lets you delete the twin. Don't "fix" a faked cursor in place — top-root
|
|
it and collapse to one render fn. (See `render-functions.md`.)
|
|
|
|
## Edit Clojure with clojure-mcp tools, not the file editor
|
|
|
|
`clojure_edit` / `clojure_edit_replace_sexp`. If a file won't compile: `clj-paren-repair`
|
|
the file, then retry; if still broken, `lein cljfmt check`. Run tests via `clojure-eval` /
|
|
`clj-nrepl-eval -p PORT`, never `lein test` (slow, last resort).
|
|
|
|
## Solr/typeahead in tests
|
|
|
|
Account/vendor search is backed by Solr, unavailable in tests. To drive a typeahead in
|
|
e2e: type under the 3-char threshold, then inject a result into Alpine state
|
|
(`Alpine.$data(el).elements = [{value, label}]`) and click it — the real click handler,
|
|
`tippy.hide()`, Alpine reactivity, and the HTMX swap all run as in production. Entity ids
|
|
come from `GET /test-info`.
|
|
|
|
---
|
|
|
|
## Scorecard exceptions (ratchet violations with a reason)
|
|
|
|
_None yet._ Append here if a migration must let a metric regress for a documented reason.
|