refactor: convert filter form from hidden to visible structure

Restructured the bookshelf filter form to be a proper visible form
instead of individual inputs with HTMX attributes pointing to a
hidden form.

Changes:
- Wrapped all filter inputs in a visible <form id="filter-form">
  with hx-get="/api/media-items/search" and hx-target="#books-grid"
- Removed redundant HTMX attributes from individual inputs since
  they're now part of the form
- Added "Search" submit button to explicitly trigger form submission
- Moved hidden pagination state inputs (limit, offset) inside the form
- Preserved all existing functionality: autocomplete, fuzzy search,
  saved filters, clear filters button
- Added checked attribute to has_cover checkbox for default state

This change fixes the architectural issue where filter inputs were
outside the form and relied on hx-include, which was fragile and
made form handling complex. The new structure is more maintainable
and follows standard HTML form patterns.

The form now properly includes all filter parameters when submitted,
ensuring that search, filters, and pagination work correctly together.
This commit is contained in:
2026-03-27 18:08:01 -04:00
parent ba243c223d
commit 77cbeb0bcf
2 changed files with 248 additions and 276 deletions
+16 -44
View File
@@ -37,6 +37,7 @@ templ BookShelf(
class="mb-6 card p-4 rounded-lg border" class="mb-6 card p-4 rounded-lg border"
style="background-color: var(--bg-secondary); border-color: var(--border);" style="background-color: var(--bg-secondary); border-color: var(--border);"
> >
<form id="filter-form" hx-get="/api/media-items/search" hx-target="#books-grid">
<div class="flex flex-wrap gap-4 items-center"> <div class="flex flex-wrap gap-4 items-center">
<!-- Library Selector --> <!-- Library Selector -->
<div class="flex-1 min-w-[200px]"> <div class="flex-1 min-w-[200px]">
@@ -47,10 +48,6 @@ templ BookShelf(
id="library-select" id="library-select"
class="w-full px-3 py-2 border rounded-lg" class="w-full px-3 py-2 border rounded-lg"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
hx-get="/api/media-items/search"
hx-target="#books-grid"
hx-trigger="change"
hx-include="#filter-form"
> >
if len(libraries) == 0 { if len(libraries) == 0 {
<option value="">No libraries available</option> <option value="">No libraries available</option>
@@ -74,10 +71,6 @@ templ BookShelf(
name="q" name="q"
type="text" type="text"
placeholder="Search all fields..." placeholder="Search all fields..."
hx-get="/api/media-items/search"
hx-trigger="keyup[key=='Enter'] from:#filter-form, keyup changed delay:500ms"
hx-target="#books-grid"
hx-include="#filter-form, #library-select"
/> />
</div> </div>
<!-- Author Filter with Autocomplete --> <!-- Author Filter with Autocomplete -->
@@ -91,10 +84,6 @@ templ BookShelf(
placeholder="Filter by author" placeholder="Filter by author"
class="w-full px-3 py-2 border rounded-lg" class="w-full px-3 py-2 border rounded-lg"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
hx-get="/api/media-items/search"
hx-target="#books-grid"
hx-trigger="keyup[key=='Enter'] from:#filter-form"
hx-include="#filter-form"
list="author-datalist" list="author-datalist"
@input.debounce.300ms="if($el.value.length >= 2) fetchAuthorValues($el)" @input.debounce.300ms="if($el.value.length >= 2) fetchAuthorValues($el)"
/> />
@@ -109,10 +98,6 @@ templ BookShelf(
placeholder="Filter by tags" placeholder="Filter by tags"
class="w-full px-3 py-2 border rounded-lg" class="w-full px-3 py-2 border rounded-lg"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
hx-get="/api/media-items/search"
hx-target="#books-grid"
hx-trigger="keyup[key=='Enter'] from:#filter-form"
hx-include="#filter-form"
list="tags-datalist" list="tags-datalist"
@input.debounce.300ms="if($el.value.length >= 2) fetchTagValues($el)" @input.debounce.300ms="if($el.value.length >= 2) fetchTagValues($el)"
/> />
@@ -129,10 +114,6 @@ templ BookShelf(
// placeholder="Filter by genre" // placeholder="Filter by genre"
// class="w-full px-3 py-2 border rounded-lg" // class="w-full px-3 py-2 border rounded-lg"
// style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" // style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
// hx-get="/api/media-items/search"
// hx-target="#books-grid"
// hx-trigger="keyup[key=='Enter'] from:#filter-form"
// hx-include="#filter-form"
// list="genre-datalist" // list="genre-datalist"
// @input.debounce.300ms="if($el.value.length >= 2) fetchGenreValues($el)" // @input.debounce.300ms="if($el.value.length >= 2) fetchGenreValues($el)"
// /> // />
@@ -149,10 +130,6 @@ templ BookShelf(
placeholder="Filter by series" placeholder="Filter by series"
class="w-full px-3 py-2 border rounded-lg" class="w-full px-3 py-2 border rounded-lg"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
hx-get="/api/media-items/search"
hx-target="#books-grid"
hx-trigger="keyup[key=='Enter'] from:#filter-form"
hx-include="#filter-form"
list="series-datalist" list="series-datalist"
@input.debounce.300ms="if($el.value.length >= 2) fetchSeriesValues($el)" @input.debounce.300ms="if($el.value.length >= 2) fetchSeriesValues($el)"
/> />
@@ -169,10 +146,6 @@ templ BookShelf(
placeholder="Filter by language" placeholder="Filter by language"
class="w-full px-3 py-2 border rounded-lg" class="w-full px-3 py-2 border rounded-lg"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
hx-get="/api/media-items/search"
hx-target="#books-grid"
hx-trigger="keyup[key=='Enter'] from:#filter-form"
hx-include="#filter-form"
list="language-datalist" list="language-datalist"
@input.debounce.300ms="if($el.value.length >= 2) fetchLanguageValues($el)" @input.debounce.300ms="if($el.value.length >= 2) fetchLanguageValues($el)"
/> />
@@ -190,10 +163,6 @@ templ BookShelf(
placeholder="From" placeholder="From"
class="w-full px-3 py-2 border rounded-lg" class="w-full px-3 py-2 border rounded-lg"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
hx-get="/api/media-items/search"
hx-target="#books-grid"
hx-trigger="keyup[key=='Enter'] from:#filter-form"
hx-include="#filter-form"
/> />
<input <input
type="number" type="number"
@@ -201,25 +170,19 @@ templ BookShelf(
placeholder="To" placeholder="To"
class="w-full px-3 py-2 border rounded-lg" class="w-full px-3 py-2 border rounded-lg"
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
hx-get="/api/media-items/search"
hx-target="#books-grid"
hx-trigger="keyup[key=='Enter'] from:#filter-form"
hx-include="#filter-form"
/> />
</div> </div>
</div> </div>
<!-- Has Cover Filter --> <!-- Has Cover Filter -->
<div class="flex items-end"> <div class="flex items-end">
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input type="hidden" name="has_cover" value="false"/>
<input <input
type="checkbox" type="checkbox"
name="has_cover" name="has_cover"
value="true" value="true"
checked
class="w-4 h-4 rounded" class="w-4 h-4 rounded"
hx-get="/api/media-items/search"
hx-target="#books-grid"
hx-trigger="change"
hx-include="#filter-form"
/> />
<span class="text-sm" style="color: var(--text-primary)">Has Cover</span> <span class="text-sm" style="color: var(--text-primary)">Has Cover</span>
</label> </label>
@@ -245,6 +208,16 @@ templ BookShelf(
<option value="page_count DESC">Page Count</option> <option value="page_count DESC">Page Count</option>
</select> </select>
</div> </div>
<!-- Submit Button -->
<div class="flex items-end">
<button
type="submit"
class="px-6 py-2 rounded-lg font-medium"
style="background-color: var(--accent); color: var(--bg-primary);"
>
🔍 Search
</button>
</div>
<!-- Save Filter Button (PRESERVED) --> <!-- Save Filter Button (PRESERVED) -->
<div class="flex items-end"> <div class="flex items-end">
<button <button
@@ -313,10 +286,6 @@ templ BookShelf(
</button> </button>
</div> </div>
</div> </div>
<!-- Hidden form for HTMX include (PRESERVED) -->
<form id="filter-form" class="hidden">
<input type="hidden" name="limit" value="50"/>
<input type="hidden" name="offset" value="0"/>
</form> </form>
</div> </div>
<!-- Books Grid --> <!-- Books Grid -->
@@ -411,6 +380,9 @@ templ BookShelf(
Save Save
</button> </button>
</div> </div>
<!-- Hidden pagination state -->
<input type="hidden" name="limit" value="50"/>
<input type="hidden" name="offset" value="0"/>
</form> </form>
</div> </div>
</div> </div>
File diff suppressed because one or more lines are too long