Files
bookhoard/docs/developer/alpine-patterns.md
T
john-okeefe 974f332b7e docs: add Alpine.js SSR-first patterns guide
Add comprehensive guide for Alpine.js SSR-first patterns in Bookhoard:
- Page classification system (Type 1: 80% SSR, Type 2: SSR+Interactive,
Type 3: 80% TypeScript)
- Alpine.js usage guidelines (UI state only, no data fetching in x-init)
- HTMX integration patterns
- When to use x-show vs CSS classes
- Form handling and validation
- Modal and dropdown patterns
- Component reusability with Alpine.data()
- Alpine.store for global state (book picker example)
This documentation helps developers maintain consistency across the
codebase
and make informed decisions about when to use Alpine.js vs vanilla
JavaScript
vs HTMX for different features.
Follows PROJECT_GUIDELINES.md documentation standards.
2026-03-20 23:02:02 -04:00

2.6 KiB

Alpine.js Patterns in Bookhoard

Book Picker Modal Pattern (SSR + HTMX + Alpine.store)

For modals that need state persistence across HTMX swaps:

TypeScript

// web/src/bookPicker.ts
Alpine.store("bookPicker", {
  isOpen: false,
  selectedBooks: new Set<string>(),
  
  open() {
    this.isOpen = true;
    this.selectedBooks.clear();
    this.loadBooks();
  },
  
  close() {
    this.isOpen = false;
    this.selectedBooks.clear();
  },
  
  toggleBook(id: string) {
    if (this.selectedBooks.has(id)) {
      this.selectedBooks.delete(id);
    } else {
      this.selectedBooks.add(id);
    }
    // Trigger reactivity
    this.selectedBooks = new Set(this.selectedBooks);
  },
  
  isSelected(id: string): boolean {
    return this.selectedBooks.has(id);
  },
  
  get selectedCount(): number {
    return this.selectedBooks.size;
  }
});
Template
<!-- Open button -->
<button @click="$store.bookPicker.open()">Add Books</button>
<!-- Modal -->
<div x-show="$store.bookPicker.isOpen"
     x-transition
     @click.self="$store.bookPicker.close()"
     style="display: none;">
  
  <!-- Search with HTMX -->
  <input @input.debounce.300ms="searchBooks($el.value)"
         hx-get="/api/media-items/filtered"
         hx-target="#book-picker-grid"
         hx-include="#filter-form" />
  
  <!-- Book grid with checkboxes -->
  <div id="book-picker-grid" x-init="loadBooks()">
    <!-- Server returns HTML with checkboxes -->
  </div>
  
  <!-- Selected count -->
  <span x-text="$store.bookPicker.selectedCount"></span>
  
  <!-- Submit -->
  <button @click="$store.bookPicker.submit()">Add Selected</button>
</div>
Key Principles
-  Alpine.store persists state across HTMX DOM swaps
-  Server returns HTML with checkbox :checked attributes
-  No client-side rendering or manual DOM manipulation
-  HTMX handles dynamic content updates
-  Alpine manages UI state only
Dropdown Pattern (Local x-data)
<div x-data="{ dropdownOpen: false }">
  <button @click="dropdownOpen = !dropdownOpen">Toggle</button>
  <div x-show="dropdownOpen"
       @click.outside="dropdownOpen = false"
       x-transition
       style="display: none;">
    <!-- Dropdown content -->
  </div>
</div>
Best Practices
-  State lives in template (x-data)
-  UI updates automatically (x-show)
-  No manual DOM manipulation in TypeScript
-  Pure business logic only in TypeScript functions
-  Use Alpine.store for global state
-  Use local x-data for component state
-  Use x-init for setup only (no data fetch for SSR pages)
-  Never use classList in TypeScript
-  Never use getElementById for show/hide
-  Never fetch data in x-init (for SSR pages)