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.
14 KiB
Collections Cleanup: Fix Alpine.js and DOMContentLoaded Issues
Overview
This guide fixes console errors caused by:
- Dead Alpine.js exports (functions that don't exist)
- DOMContentLoaded listeners running on wrong pages
- 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
-
Start the server:
podman compose up -d -
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
- Navigate to
-
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 exportsweb/src/analytics.ts- Removed DOMContentLoadedweb/src/docs.ts- Removed DOMContentLoadedweb/src/library.ts- Removed DOMContentLoadedweb/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.gofiles 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:
- Create
templates/add_books_modal.templ - Add route in
internal/router/collections.go - Create handler in
internal/handlers/collections.go - Update
collections.templto use HTMX modal instead of inline - Remove all book selection JavaScript from
collections.ts
But for now, the client-side approach works fine.