Files
bookhoard/COLLECTIONS_CLEANUP_GUIDE.md
T
john-okeefe ab2e2427cc docs(ssr): create SSR-first Alpine.js guide and update cleanup guide
Created comprehensive SSR_FIRST_ALPINE_GUIDE.md to establish SSR-first
architecture principles for Alpine.js integration.

New Guide: SSR_FIRST_ALPINE_GUIDE.md

Covers:
- SSR-first principles (state in templates, no fetch in x-init for SSR pages)
- Three page type classifications:
  * Type 1: 80% SSR (Collections, Conflicts) - backend provides all data
  * Type 2: SSR + Interactive (Dashboard, Admin Library) - SSR + interactivity
  * Type 3: 80% JavaScript (Analytics) - x-init fetches all data (intentional)
- The SSR data fetch problem (x-init replacing SSR content)
- DOMContentLoaded cleanup strategies
- Page-by-page strategy for each type
- Authentication & SSR (server-side token injection)
- Verification checklist and testing approach
- Architecture diagram showing data flow

Key Principles:
-  NEVER fetch data in x-init if data is already SSR'd
-  x-init ONLY for setup (event listeners, modals)
-  Data fetch ONLY after user actions
-  State lives in template (x-data), not TypeScript

Updated: COLLECTIONS_CLEANUP_GUIDE.md

Changes:
- Added reference to SSR_FIRST_ALPINE_GUIDE.md as authority
- Removed two-option approach (no more choices)
- Documented that admin library SSR bug is already fixed (commit 1b9bc64)
- Simplified dashboard approach (wrap existing code in initDashboard)
- Simplified docs approach (simple setup, no data fetch)
- Updated summary to reflect completed work
- Added architecture section showing state location

Architecture Clarity:
- Templates: UI state (x-data, x-show)
- Backend: SSR data
- TypeScript: Business logic only
- No hybrid approach - follow SSR-first principles

References:
- SSR_FIRST_ALPINE_GUIDE.md - Complete SSR-first architecture
- ALPINE_COMPLETION_GUIDE.md - Full Alpine.js migration (future goal)
- PROJECT_GUIDELINES.md - Project standards

This establishes a single source of truth for SSR-first Alpine.js
architecture and removes confusion about which approach to use.
2026-03-12 18:07:16 -04:00

17 KiB
Raw Blame History

Collections and Alpine.js Cleanup Guide

Overview

This guide provides fixes for Alpine.js issues while maintaining SSR-first architecture.

References:

  • SSR_FIRST_ALPINE_GUIDE.md - Complete SSR-first architecture principles (READ THIS FIRST)
  • ALPINE_COMPLETION_GUIDE.md - Full reactive Alpine.js migration (eventual goal)

Problem Statement

Console errors caused by:

  1. Dead Alpine.js exports (functions that don't exist)
  2. DOMContentLoaded listeners running on wrong pages
  3. Missing x-init calls in templates
  4. CRITICAL: x-init functions that fetch data and replace SSR content

SSR-First Principles

See SSR_FIRST_ALPINE_GUIDE.md for complete documentation

Quick Summary:

  • NEVER fetch data in x-init if data is already SSR'd
  • x-init ONLY for setup (event listeners, modals)
  • Data fetch ONLY after user actions (create/delete/update)
  • State lives in template (x-data), not in TypeScript

Page Types

Type 1: 80% SSR (Collections, Conflicts, Queue)

  • Backend provides all data
  • Alpine for modals only
  • No data fetch in x-init

Type 2: SSR + Interactive (Dashboard, Admin Library)

  • Backend provides initial data
  • Alpine for interactivity (drag-drop, CRUD)
  • x-init sets up listeners only

Type 3: 80% JavaScript (Analytics)

  • Backend renders empty shell
  • x-init fetches ALL data (intentional)

Review Status

Pages methodically reviewed so far:

  • Dashboard - Completed
  • 🔄 Collections - In progress (this guide)
  • ⏸️ Admin Library - Fixed (see SSR_FIRST_ALPINE_GUIDE.md)
  • ⏸️ Other pages - Not yet reviewed

Goal: Fix console errors and maintain SSR architecture.


Prerequisites

Before starting, verify the current state:

# Check current errors
cd /home/nymusicman/Code/bookhoard
npm run build:ts

# Should see errors about:
# - "addbooksToAdd is not defined"
# - "removebooksToAdd is not defined"
# - "toggleBookSelection is not defined"
# - etc.

Step 1: Fix collections.ts Alpine.data Export

File: web/src/collections.ts

Problem: Alpine.data exports functions that were deleted in commit 93710a1.

Action: Update the Alpine.data export to only include existing functions.

Step 1.1: Read Current Alpine.data Export

# Check what's currently exported
tail -50 web/src/collections.ts | grep -A25 "Alpine.data"

You'll see something like:

Alpine.data("collections", () => ({
  addbooksToAdd,           // ❌ Does NOT exist - deleted
  backToCollections,
  closeCollectionModal,
  createRule,
  deleteRule,
  filterCollectionBooks,
  filterIcons,
  hideAddBooksModal,
  initCollectionDetail,     // ❌ Does NOT exist - deleted
  initColorSelection,      // ❌ Does NOT exist - deleted
  initIconSelection,        // ❌ Does NOT exist - deleted
  loadCollectionRules,
  loadCollections,
  navigateToCollection,
  populateIconGrid,
  removeBook,
  removebooksToAdd,         // ❌ Does NOT exist - deleted
  searchBooksForCollections,
  selectColor,
  selectIcon,
  showAddBooksModal,
  showAllIcons,              // ❌ Does NOT exist - deleted
  setupHTMXAuth,
  testRule,
  toggleBookForRemoval,    // ❌ Does NOT exist - deleted
  toggleBookSelection,      // ❌ Does NOT exist - deleted
  updateSelectedCount,
}));

Step 1.2: Read Current Export Statement

# Check the export statement at the end of the file
grep -A30 "^export {" web/src/collections.ts

You'll see similar dead exports.

Step 1.3: Verify Which Functions Actually Exist

# Search for function definitions
grep -n "^function\|^async function" web/src/collections.ts

Expected output (actual existing functions):

  • backToCollections
  • closeCollectionModal
  • createRule
  • deleteRule
  • filterCollectionBooks
  • filterIcons
  • hideAddBooksModal
  • loadCollectionRules
  • loadCollections
  • navigateToCollection
  • populateIconGrid
  • removeBook
  • searchBooksForCollections
  • selectColor
  • selectIcon
  • showAddBooksModal
  • setupHTMXAuth
  • testRule
  • updateSelectedCount

Step 1.4: Update the Export Statement

Line 407: Change the export statement to only include existing functions:

export {
  backToCollections,
  closeCollectionModal,
  createRule,
  deleteRule,
  filterCollectionBooks,
  filterIcons,
  hideAddBooksModal,
  loadCollectionRules,
  loadCollections,
  navigateToCollection,
  populateIconGrid,
  removeBook,
  searchBooksForCollections,
  selectColor,
  selectIcon,
  showAddBooksModal,
  setupHTMXAuth,
  testRule,
  updateSelectedCount,
};

Step 1.5: Update Alpine.data Registration

Line 424: Update Alpine.data to match the export:

Alpine.data("collections", () => ({
  backToCollections,
  closeCollectionModal,
  createRule,
  deleteRule,
  filterCollectionBooks,
  filterIcons,
  hideAddBooksModal,
  loadCollectionRules,
  loadCollections,
  navigateToCollection,
  populateIconGrid,
  removeBook,
  searchBooksForCollections,
  selectColor,
  selectIcon,
  showAddBooksModal,
  setupHTMXAuth,
  testRule,
  updateSelectedCount,
}));

Step 1.6: Verify the Fix

# Build TypeScript
npm run build:ts

# Should now succeed with 0 errors

Step 2: Fix Template Function Calls

File: templates/collections.templ

Problem: Template calls functions that no longer exist.

Step 2.1: Remove Dead Function Calls from Collection Detail Page

Line 226: Remove the removebooksToAdd call:

<!-- BEFORE -->
<button
  id="bulk-remove-btn"
  @click="removebooksToAdd"
  disabled
  class="btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
  🗑️ Remove Selected
</button>

<!-- AFTER -->
<button
  id="bulk-remove-btn"
  disabled
  class="btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
  🗑️ Remove Selected
</button>

Line 229: Remove addbooksToAdd call:

<!-- BEFORE -->
<button @click="addbooksToAdd" class="btn-primary px-4 py-2 rounded-lg">
   Add Selected Books
</button>

<!-- AFTER -->
<button type="button" class="btn-primary px-4 py-2 rounded-lg" style="opacity: 0.5; cursor: not-allowed;" disabled>
   Add Selected Books
</button>

Step 2.2: Regenerate Templates

# Generate Go template files
templ generate

# Should see: Complete [updates=0 duration=~40ms]

Step 3: Fix Other TypeScript Files (Remove DOMContentLoaded)

For each file, we'll remove the DOMContentLoaded listener and add x-init to the template.

Step 3.1: analytics.ts

File: web/src/analytics.ts

Current:

export { loadAnalytics };

document.addEventListener("DOMContentLoaded", loadAnalytics);

Alpine.data("analytics", () => ({
  loadAnalytics,
}));

Change to:

export { loadAnalytics };

// REMOVE this line:
// document.addEventListener("DOMContentLoaded", loadAnalytics);

Alpine.data("analytics", () => ({
  loadAnalytics,
}));

Template Update: templates/analytics.templ

Line 14: Add x-init to body tag:

<!-- BEFORE -->
<body x-data="analytics" class="theme-{ user.Theme }">

<!-- AFTER -->
<body x-data="analytics" x-init="loadAnalytics" class="theme-{ user.Theme }">

Step 3.2: docs.ts - SIMPLE SETUP ONLY

File: web/src/docs.ts

Good news: initializeDocsSearch() ONLY sets up an event listener - no data fetch!

Current (around line 95-100):

document.addEventListener("DOMContentLoaded", () => {
  initializeDocsSearch();
});

Solution: Remove DOMContentLoaded, use x-init

Remove DOMContentLoaded:

// DELETE:
// document.addEventListener("DOMContentLoaded", () => {
//   initializeDocsSearch();
// });

export { toggleSidebar, initializeDocsSearch };

Alpine.data("docs", () => ({
  toggleSidebar,
  initializeDocsSearch,  // Keep as-is
}));

Update template:

<!-- templates/docs.templ -->
<body x-data="docs" x-init="initializeDocsSearch" class="theme-{ user.Theme }">

Simple setup only No data fetch (search is client-side) x-init is appropriate here

Step 3.3: library.ts - ALREADY FIXED

Status: COMPLETED - See commit 1b9bc64

The SSR bug has been fixed:

  • Removed void reloadLibraries() from initializeLibraryAdmin()
  • SSR provides initial library list (no fetch on page load)
  • reloadLibraries() available for after CRUD operations
  • See SSR_FIRST_ALPINE_GUIDE.md for complete SSR-first principles

Current state (web/src/library.ts:653-655):

function initializeLibraryAdmin(): void {
  // Setup event listeners
  const librariesList = document.getElementById("libraries-list");
  if (librariesList) {
    librariesList.addEventListener("click", handleLibraryListClick);
  }

  // ... setup code ...

  // ✅ FIXED: No data fetch - SSR provides initial library list
  // reloadLibraries() is called AFTER create/delete/update operations only
}

Template (templates/admin_library.templ:11):

<body x-data="library" x-init="initializeLibraryAdmin" class="theme-{ user.Theme }">

No changes needed - SSR bug is already fixed.

Step 3.4: dashboard.ts - WRAP EXISTING CODE

File: web/src/dashboard.ts

Good news: Dashboard already uses event delegation with data-action attributes!

Current (lines 494-521):

document.addEventListener("DOMContentLoaded", () => {
  initDragAndDrop();

  document.addEventListener("click", (e: Event) => {
    // ... event delegation with data-action ...
  });

  // ... library select setup ...
});

Solution: Wrap existing DOMContentLoaded code in initDashboard() function

Create wrapper function at end of dashboard.ts:

function initDashboard() {
  initDragAndDrop();  // Setup drag-drop

  document.addEventListener("click", (e: Event) => {
    const target = e.target as HTMLElement;
    const actionElem = target.closest("[data-action]") as HTMLElement;
    const action = actionElem?.getAttribute("data-action");

    switch (action) {
      case "scroll-carousel": /* ... */ break;
      // ... existing cases ...
    }
  });

  document.addEventListener("input", (e: Event) => {
    // ... existing input handler ...
  });

  // Load saved library on page load
  const savedLibrary = localStorage.getItem("selectedLibrary");
  const currentLibrary = new URLSearchParams(window.location.search).get("library_id");
  if (savedLibrary && savedLibrary !== currentLibrary) {
    window.location.href = `/dashboard?library_id=${savedLibrary}`;
  }
}

export { initDashboard };

Alpine.data("dashboard", () => ({
  initDashboard,
}));

Update template:

<!-- templates/dashboard.templ line 19 -->
<body x-data="dashboard" x-init="initDashboard" class="theme-{ user.Theme }">

Keeps event delegation pattern Wraps existing code (minimal changes) x-init does NOT fetch data (SSR provides initial dashboard) localStorage redirect is user preference, not data fetch

Create a wrapper function at the end of the file:

Add before the export statement (before line ~490):

// Wrapper function for dashboard initialization
function initDashboard() {
  initDragAndDrop();

  document.addEventListener("click", (e: Event) => {
    const target = e.target as HTMLElement;
    const actionElem = target.closest("[data-action]") as HTMLElement;
    const action = actionElem?.getAttribute("data-action");

    switch (action) {
      case "scroll-carousel": {
        const collectionId =
          target.dataset.direction || actionElem?.dataset.direction || "0";
        if (collectionId) scrollCarousel(collectionId, direction);
        break;
      }
      // ... keep all existing cases ...
    }
  });

  document.addEventListener("input", (e: Event) => {
    const target = e.target as HTMLElement;
    const actionElem = target.closest("[data-input-action]") as HTMLElement;
    const action = actionElem?.getAttribute("data-input-action");

    switch (action) {
      case "update-items-count": {
        const input = target as HTMLInputElement;
        const displayTarget = input.getAttribute("target");
        if (displayTarget) updateItemsCount(input, displayTarget);
        break;
      }
    }
  });

  // Load saved library on page load
  const librarySelect = document.getElementById(
    "library-select",
  ) as HTMLSelectElement;
  if (librarySelect) {
    librarySelect.addEventListener("change", (e) => {
      const target = e.target as HTMLSelectElement;
      if (target.value) {
        switchLibrary(target.value);
      }
    });
  }

  const savedLibrary = localStorage.getItem("selectedLibrary");
  const currentLibrary = new URLSearchParams(window.location.search).get(
    "library_id",
  );
  if (savedLibrary && savedLibrary !== currentLibrary) {
    window.location.href = `/dashboard?library_id=${savedLibrary}`;
  }
}

Remove the old DOMContentLoaded block (delete lines 494-521):

// DELETE this entire block:
// document.addEventListener("DOMContentLoaded", () => {
//   initDragAndDrop();
//   ... all 70+ lines ...
// });

Add to export statement:

export {
  closeDashboardSettings,
  openDashboardSettings,
  initDashboard,  // ← ADD THIS
  saveDashboardSettings,
  scrollCarousel,
  // ... keep all other exports ...
};

Add to Alpine.data:

Alpine.data("dashboard", () => ({
  closeDashboardSettings,
  openDashboardSettings,
  initDashboard,  // ← ADD THIS
  saveDashboardSettings,
  scrollCarousel,
  // ... keep all other exports ...
}));

Template Update: templates/dashboard.templ

Line 19: Add x-data and x-init to body tag:

<!-- BEFORE -->
<body class="theme-{ user.Theme }">

<!-- AFTER -->
<body class="theme-{ user.Theme }" x-data="dashboard" x-init="initDashboard()">

Step 3.5: Verify All TypeScript Files

# Build all TypeScript
npm run build:ts

# Should succeed with 0 errors

Step 4: Regenerate Templates

# Generate Go template files
templ generate

# Expected: Complete [updates=X duration=~40ms]
# where X is the number of templates modified

Step 5: Build and Verify

# Build the Go server
go build ./cmd/server

# If build succeeds, you're done!
# If build fails, check the error and fix accordingly

Step 6: Test in Browser

  1. Start the server:

    podman compose up -d
    
  2. Open browser and test:

    • Navigate to /collections - should work with no console errors
    • Navigate to /analytics - should load analytics data
    • Navigate to /dashboard - drag and drop should work
    • Navigate to /docs - search should work
    • Navigate to /admin/library - library management should work
  3. Check browser console:

    • No "X is not defined" errors
    • No "n.bind is not a function" errors
    • No "initX is not defined" errors

Summary

Progress So Far

Pages methodically reviewed so far:

  • Dashboard - Completed, working correctly with SSR
  • Admin Library - SSR bug fixed (commit 1b9bc64)
  • 🔄 Collections - In progress (this guide)
  • ⏸️ Other pages - Not yet reviewed, will be done page-by-page

Completed Changes

TypeScript Files (3 files):

  • web/src/collections.ts - Removed dead Alpine.js exports
  • web/src/analytics.ts - Removed DOMContentLoaded, SSR-first (intentional data fetch)
  • web/src/admin.ts - Removed DOMContentLoaded, WebSocket moved to template
  • web/src/library.ts - Fixed SSR bug, removed reloadLibraries() from init

Template Files (1 file):

  • templates/analytics.templ - Added x-init="loadAnalytics" (JS-heavy page, correct)

Pending Fixes

Collections (This Guide):

  • Dead exports removed (Step 1)
  • DOMContentLoaded cleanup (Step 3)
  • x-init calls needed (Step 3)

Dashboard:

  • Wrap existing code in initDashboard() function
  • Add x-init to template
  • Already uses event delegation correctly

Docs:

  • Remove DOMContentLoaded
  • Add x-init="initializeDocsSearch" to template
  • Simple setup only, no data fetch

What Was NOT Changed

  • "Add Books" modal - Still client-side Alpine.js (quick fix decision)
  • WebSocket code - Already moved to templates with server-side token injection
  • API endpoints - Already exist and work correctly
  • HTMX modals - Already implemented for collection CRUD

SSR-First Principles

See SSR_FIRST_ALPINE_GUIDE.md for complete documentation:

Type 1 (80% SSR): Collections, Conflicts - backend provides data, Alpine for modals only Type 2 (SSR + Interactive): Dashboard, Admin Library - SSR data + Alpine for interactivity Type 3 (80% JS): Analytics - x-init fetches data (intentional)

Key Principle:

  • NEVER fetch data in x-init if data is already SSR'd
  • x-init ONLY for setup (event listeners, modals)
  • Data fetch ONLY after user actions (create/delete/update)

Next Steps

  1. Complete collections fixes (this guide)
  2. Continue page-by-page review - one page at a time
  3. Reference SSR_FIRST_ALPINE_GUIDE.md for SSR-first principles
  4. Reference ALPINE_COMPLETION_GUIDE.md for full Alpine.js migration (future goal)

Architecture

State location:

  • Templates: UI state (x-data, x-show)
  • Backend: SSR data
  • TypeScript: Business logic only

No hybrid approach:

  • Follow SSR-first principles
  • Full Alpine.js is eventual goal (see ALPINE_COMPLETION_GUIDE.md)
  • Fix issues methodically as you encounter them