Files
bookhoard/COLLECTIONS_CLEANUP_GUIDE.md
T
john-okeefe bfdef1396a docs(collections): add comprehensive cleanup guide for Alpine.js and DOMContentLoaded issues
Add detailed step-by-step guide for fixing console errors in collections
and cleaning up DOMContentLoaded listeners across multiple TypeScript files.

COLLECTIONS_CLEANUP_GUIDE.md provides:
- Complete analysis of what was broken and why
- Line-by-line instructions for fixing collections.ts Alpine.data exports
- Step-by-step guide for removing DOMContentLoaded from 5 TypeScript files
- Template x-init additions for proper Alpine.js initialization
- Verification and testing steps

This guide documents the fix for:
- Dead Alpine.js exports (addbooksToAdd, removebooksToAdd, toggleBookSelection, etc.)
- DOMContentLoaded listeners running on wrong pages (analytics, docs, library, dashboard, admin)
- Missing x-init calls in templates (analytics, docs, dashboard, library)
- Template cleanup (removing dead function calls in collections.templ)

The guide follows PROJECT_GUIDELINES.md standards with clear code examples,
file paths, and verification steps. It serves as both implementation guide
and documentation for the cleanup effort.
2026-03-12 17:21:28 -04:00

14 KiB
Raw Blame History

Collections Cleanup: Fix Alpine.js and DOMContentLoaded Issues

Overview

This guide fixes 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

Goal: Clean, working Alpine.js integration with proper 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

File: web/src/docs.ts

Current (around line 95-100):

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

export { toggleSidebar, initializeDocsSearch };

Alpine.data("docs", () => ({
  toggleSidebar,
  initializeSearch: initializeDocsSearch,
}));

Change to:

// REMOVE this entire block:
// document.addEventListener("DOMContentLoaded", () => {
//   initializeDocsSearch();
// });

export { toggleSidebar, initializeDocsSearch };

Alpine.data("docs", () => ({
  toggleSidebar,
  initializeSearch: initializeDocsSearch,
}));

Template Update: templates/docs.templ

Find the <body> tag and add x-init:

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

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

Step 3.3: library.ts

File: web/src/library.ts

Current (around line 655-662):

// Initialize on DOM ready
if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", initializeLibraryAdmin);
} else {
  initializeLibraryAdmin();
}

// ... later in file ...

Change to:

// REMOVE this entire block:
// if (document.readyState === "loading") {
//   document.addEventListener("DOMContentLoaded", initializeLibraryAdmin);
// } else {
//   initializeLibraryAdmin();
// }

Template Update: templates/library.templ

Find the <body> tag and add x-data and x-init:

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

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

Step 3.4: dashboard.ts

File: web/src/dashboard.ts

This file needs special handling since it has a complex DOMContentLoaded block with multiple event listeners.

Current (lines 494-521):

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

  document.addEventListener("click", (e: Event) => {
    // ... 70+ lines of event handling ...
  });

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

  // ... more initialization ...
});

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

Changes Made

TypeScript Files (5 files):

  • web/src/collections.ts - Removed dead exports
  • web/src/analytics.ts - Removed DOMContentLoaded
  • web/src/docs.ts - Removed DOMContentLoaded
  • web/src/library.ts - Removed DOMContentLoaded
  • web/src/dashboard.ts - Created initDashboard wrapper, removed DOMContentLoaded

Template Files (5 files):

  • templates/analytics.templ - Added x-init="loadAnalytics"
  • templates/docs.templ - Added x-init="initializeSearch"
  • templates/dashboard.templ - Added x-data="dashboard" x-init="initDashboard()"
  • templates/library.templ - Added x-data="library" x-init="reloadLibraries"
  • templates/collections.templ - Removed dead function calls

Generated Files:

  • All _templ.go files regenerated

What Was NOT Changed

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

Result

No more Alpine.js errors
No more DOMContentLoaded pollution
Pages run only their own initialization code
Console is clean
SSR architecture maintained

Next Steps (Optional)

If you want to convert the "Add Books" modal to HTMX (future enhancement), that would require:

  1. Create templates/add_books_modal.templ
  2. Add route in internal/router/collections.go
  3. Create handler in internal/handlers/collections.go
  4. Update collections.templ to use HTMX modal instead of inline
  5. Remove all book selection JavaScript from collections.ts

But for now, the client-side approach works fine.