refactor(ssr): full Selmer migration of Transaction Edit; remove the wizard
Migrate every part of the Transaction Edit modal's HTML to Selmer templates
(zero Hiccup in the render path) and delete the mm multi-modal "wizard"
abstraction entirely -- there was only ever one step.
- New auto-ap.ssr.components.selmer (sc) + ~22 shared component partials under
resources/templates/components/ (typeahead, button-group, radio-card,
data-grid, validated-field, modal, buttons, inputs, SVGs). Each wrapper renders
its own partial; dynamic HTMX/Alpine attrs bridge via attrs->str -> {{attrs|safe}}.
- 15 modal templates under resources/templates/transaction-edit/.
- Delete EditWizard/LinksStep records + all mm/* usage. Plain handlers: flat
wrap-decode-edit (fields renamed off step-params[...], stray keys stripped),
flat wrap-derive-state, *errors*-based field errors, generic wrap-form-4xx-2.
- Drop the edit-wizard-navigate route (routes ~12 -> 5).
- Fix: stray `method` (tab button-group hidden) leaked into the upsert -> 500;
strip decoded map to schema keys.
- e2e selectors updated (#wizard-form->#edit-form, #wizardmodal->#editmodal,
step-params[...] field names). Parity: swap 6/6, edit 8/8, suite 38/1
(1 pre-existing unrelated nav test).
- ssr-form-migration skill updated with the learnings (composition mechanics,
sc/* library, drop-the-wizard recipe, scorecard row, 3 new gotchas).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -75,11 +75,60 @@ Lessons:
|
||||
- **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
|
||||
## Composition — verified mechanics (selmer 1.12.61)
|
||||
|
||||
Selmer composes via `{% include %}` and `{% block %}`. Prefer small per-component templates
|
||||
referenced by classpath path. Keep `|safe` to values the server fully controls (rendered
|
||||
Hiccup, JSON for `x-data`), never raw user input.
|
||||
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user