refactor(ssr): make the bulk-code form-ctx data-only

Move the remaining static markup out of the bulk-code form view-model and into
the templates, leaving form-ctx as plain data (plus a urls map and two button
contexts). The form/vendor hx-wiring, the status <option> list, the per-row
transition / location-swap / remove wiring, and the field names are now literal
in the templates, built from the row index and the shared urls.

- form.html: form attrs literal; ids render name="ids[N]" via forloop.counter0.
- body.html: vendor-changed wiring literal; status is an inline <select> with
  literal options (selected via {% if status.value = ... %}); field wrappers use
  {% if has_error %}has-error.
- account-row.html: the <tr> transitions, db/id hidden, location-cell swap and
  remove <a> are literal with {{ row.index }} / {{ urls.changed }}; only the
  Alpine x-data, errors, and the typeahead/location/money control contexts are
  passed as data.
- form-ctx / account-row-vm reduced to data; drop the now-unused
  sc/validated-field-classes.

Tradeoff: the status <select> and the remove <a> inline the shared base classes
(those partials can't take literal option labels / per-row wiring), so those two
class strings are duplicated in the bulk-code templates.

Verified: moved wiring correct by targeted checks (ids[N], form/vendor hx-*,
account-row-N, location swap + remove with index, status selected, no unrendered
tags); full browser flow green -- open (3 ids), vendor auto-populate, status
set+persist, add/remove row, submit "Transactions Coded", no JS errors. Shared
component class-sets unchanged (this commit only touches bulk-code).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 22:48:54 -07:00
parent f16c52d70b
commit e1a2f7b638
5 changed files with 58 additions and 100 deletions

View File

@@ -1,27 +1,34 @@
{# One expense-account row, read from a loop-bound `row` view-model (account-row-vm). The
account typeahead, location select, and remove button all reuse the shared component
partials (typeahead.html / location-select.html / a-icon-button via its ctx); only the
table layout is inline. The location cell (#account-location-N) swaps just itself on
{# One expense-account row from a loop-bound `row` view-model. All structure, wiring, and
field names are literal here, built from `row.index` + the shared `urls`; only data (the
Alpine x-data, db/id, errors, and the typeahead / location / money control contexts)
comes from the view-model. The location cell (#account-location-N) swaps just itself on
account change; the remove button swaps the whole #bulk-code-form. #}
<tr class="{{ row.tr_classes }}"{{ row.tr_attrs|safe }}>
<input type="hidden" name="{{ row.db_id_name }}"{% if row.db_id_value %} value="{{ row.db_id_value }}"{% endif %}>
<tr class="account-row border-b dark:border-gray-600 group hover:bg-gray-100 dark:hover:bg-gray-700"
id="account-row-{{ row.index }}" x-data="{{ row.x_data }}" x-ref="p" x-show="show"
x-init="$nextTick(() => show=true)"
x-transition:enter="transition-opacity duration-500" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
x-transition:leave="transition duration-500" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
<input type="hidden" name="accounts[{{ row.index }}][db/id]"{% if row.db_id_value %} value="{{ row.db_id_value }}"{% endif %}>
<td class="px-4 py-2">
<div class="{{ row.account_field_classes }}">
<div class="flex flex-col">{% with width=row.account.width x_data=row.account.x_data x_model=row.account.x_model key=row.account.key disabled=row.account.disabled a_xinit=row.account.a_xinit placeholder=row.account.placeholder hidden_attrs=row.account.hidden_attrs %}{% include "templates/components/typeahead.html" %}{% endwith %}</div>
<div class="group {% if row.account_has_error %}has-error {% endif %}">
<div class="flex flex-col">{% with width="" x_data=row.account.x_data x_model=row.account.x_model key=row.account.key disabled=row.account.disabled a_xinit=row.account.a_xinit placeholder=row.account.placeholder hidden_attrs=row.account.hidden_attrs %}{% include "templates/components/typeahead.html" %}{% endwith %}</div>
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ row.account_error }}</p>
</div>
</td>
<td class="px-4 py-2" id="{{ row.location_cell_id }}">
<div class="{{ row.location_field_classes }}"{{ row.location_field_attrs|safe }}>
{% with name=row.location.name variant=row.location.variant options=row.location.options %}{% include "templates/components/location-select.html" %}{% endwith %}
<td class="px-4 py-2" id="account-location-{{ row.index }}">
<div class="group {% if row.location_has_error %}has-error {% endif %}"
hx-post="{{ urls.changed }}" hx-target="#account-location-{{ row.index }}" hx-select="#account-location-{{ row.index }}"
hx-vals='{"name":"accounts[{{ row.index }}][location]"{% if client_id %},"client-id":{{ client_id }}{% endif %}}'
x-hx-val:account-id="accountId" x-dispatch:changed="accountId" hx-trigger="changed" hx-swap="outerHTML" hx-include="closest form">
{% with name=row.location.name variant="w-full" options=row.location.options %}{% include "templates/components/location-select.html" %}{% endwith %}
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ row.location_error }}</p>
</div>
</td>
<td class="px-4 py-2">
<div class="{{ row.pct_field_classes }}">
<div class="group {% if row.pct_has_error %}has-error {% endif %}">
{% with variant=row.pct.variant attrs=row.pct.attrs %}{% include "templates/components/money-input.html" %}{% endwith %}
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ row.pct_error }}</p>
</div>
</td>
<td class="px-4 py-2 align-top">{% with extra=row.remove.extra attrs=row.remove.attrs body=row.remove.body %}{% include "templates/components/a-icon-button.html" %}{% endwith %}</td>
<td class="px-4 py-2 align-top"><a href="" class="account-remove-action p-3 inline-flex items-center justify-center bg-white dark:bg-gray-600 items-center text-sm font-medium border border-gray-300 dark:border-gray-700 text-center text-gray-500 hover:text-gray-800 rounded-lg dark:text-gray-400 dark:hover:text-gray-100" hx-post="{{ urls.changed }}" hx-vals='{"op":"remove-account","row-index":{{ row.index }}}' hx-target="#bulk-code-form" hx-select="#bulk-code-form" hx-swap="outerHTML" hx-include="closest form"><div class="h-4 w-4">{% include "templates/components/svg-x.html" %}</div></a></td>
</tr>