Release Notes — v0.2.14 (July 6, 2026)
1. Tab-Preserving Form Save Response for Detail Views
When saving a form on a BaseDetailView with tab groups, the HTMX response previously returned main_object_form.html — a flat form partial with no tab structure. This caused the #form-container innerHTML replacement to destroy the Alpine.js tab bar and all tab-pane layout. A new dedicated template now preserves the tab structure in the save response.
Problem
- Form action URL included
?edit=1, causingget_template_names()to returnmain_object_form.htmlregardless of whether the view had tab groups. main_object_form.htmlrenders all fieldtabs in a single flat accordion — no tab bar, no tab panes.- The
submit_main_object_form_form()JS replaced#form-containerinnerHTML with the flat response, destroying the Alpine.js tab component entirely. - After save, the user saw a flat form without tabs and had to reload the page to restore the tab layout.
Fix
New Template: core/partials/detail_form_tabs_partial.html
A new partial template that wraps form fields and inline sections inside the Alpine.js tab structure, identical to the tab portion of detail_partial.html. Includes:
- Alpine.js tab bar (
x-data="{ tab: '{{ tab_groups.0.key }}' }") withx-showpanes fieldtabs_accordion.htmlrendered per-group inside tab panesinline_items_section.htmlrendered per-group for tab-assigned inlines- TomSelect initialization script (
initTomSelects())
Template Resolution (base.py:364-370)
BaseDetailView.get_template_names() now checks get_tab_groups() when edit=1:
def get_template_names(self):
if self.request.GET.get("edit") == "1":
if self.get_tab_groups():
return ["core/partials/detail_form_tabs_partial.html"]
return [self.main_object_form_template_partial]
return [self.template_name]
- Tabs exist +
edit=1→detail_form_tabs_partial.html(tab structure preserved) - No tabs +
edit=1→main_object_form.html(flat form, existing behavior) - No
edit=1→self.template_name(full detail page, unchanged)
Backward Compatibility
- Views without
get_tab_groups()(or returning[]) continue usingmain_object_form.html— no behavioral change. - Views WITH tab groups now get a tab-preserving response — a fix, not a breaking change.
Files changed: 2 (1 new template, 1 view).
2. HTMX Processing After Form Save — Inline List Pages Not Loading
After form save, inline items rendered via inline_items_section.html were not loading their content. The #form-container innerHTML replacement inserted new hx-get / hx-trigger="load" elements, but HTMX's MutationObserver did not process them because innerHTML assignment does not guarantee mutation observer firing in all browsers. An explicit htmx.process() call was missing.
Fix
Template: core/partials/detail_partial.html:104-106
Added htmx.process(targetElement) immediately after innerHTML replacement, before initTomSelects():
targetElement.innerHTML = html;
if (typeof htmx !== 'undefined') {
htmx.process(targetElement);
}
if (typeof initTomSelects === 'function') {
initTomSelects(targetElement);
}
Pattern Consistency
This matches the existing pattern used in:
- journal/partials/journal_batch_detail.html:164 — if (typeof htmx !== 'undefined') htmx.process(target);
- core/partials/inline_update_partial.html:202 — htmx.process(target);
- core/partials/inline_details_partial.html:168 — if (typeof htmx !== 'undefined') htmx.process(target);
Processing Order
htmx.process() scans the new DOM for hx-* attributes and triggers hx-trigger="load" elements — this is what loads inline list pages via HTMX lazy loading. Running before initTomSelects() ensures HTMX does not interfere with the custom widget DOM modifications.
Files changed: 1 template.
3. TomSelect (M2M Widget) Initialization in Tabbed Form Template
The new detail_form_tabs_partial.html was missing the inline <script> tag that initializes custom TomSelect (MultiSelectWidget) instances on M2M form fields. All other form partials include this script for consistency.
Fix
Template: core/partials/detail_form_tabs_partial.html:33
Added the standard TomSelect initialization script at the end of the template:
<script>if(typeof initTomSelects==='function'){initTomSelects();}</script>
This matches the pattern in main_object_form.html:11, inline_update_partial.html:215, inline_details_partial.html:175, and all other form partials.
Dual Initialization Paths
TomSelect widgets are initialized via two paths depending on context:
| Context | Mechanism |
|---|---|
| AJAX form save (innerHTML injection) | initTomSelects(targetElement) called explicitly in detail_partial.html JS after injection |
| Full page load (server-rendered) | Inline <script> tag at end of template |
The inline script ensures M2M widgets are initialized when the tabbed edit form is loaded as a full page (e.g., initial navigation with ?edit=1).
Files changed: 1 template.
4. Version Bump
setup.py: Version bumped from0.2.13→0.2.14.
Summary
| Area | Description | Files |
|---|---|---|
| Tab-Preserving Save | New detail_form_tabs_partial.html template; get_template_names() resolves to tabbed template when tab groups exist and edit=1 |
2 |
| HTMX Processing Fix | Added htmx.process(targetElement) after innerHTML replacement for inline list page loading |
1 |
| TomSelect Init | Added initTomSelects() script to tabbed form template; M2M widgets now initialize in tabbed edit views |
1 |
| Versioning | Version bumped to 0.2.14 | 1 |
Total: 1 new template, 3 modified files, 4 files changed.
Note: All changes are uncommitted and untested. The user is solely responsible for testing and validating all outputs before committing or deploying.