# 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: ```bash # 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 ```bash # Check what's currently exported tail -50 web/src/collections.ts | grep -A25 "Alpine.data" ``` You'll see something like: ```typescript 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 ```bash # 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 ```bash # 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: ```typescript 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: ```typescript 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 ```bash # 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: ```html ``` **Line 229:** Remove `addbooksToAdd` call: ```html ``` ### Step 2.2: Regenerate Templates ```bash # 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:** ```typescript export { loadAnalytics }; document.addEventListener("DOMContentLoaded", loadAnalytics); Alpine.data("analytics", () => ({ loadAnalytics, })); ``` **Change to:** ```typescript 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: ```html ``` ### 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):** ```typescript document.addEventListener("DOMContentLoaded", () => { initializeDocsSearch(); }); ``` **Solution:** Remove DOMContentLoaded, use x-init **Remove DOMContentLoaded:** ```typescript // DELETE: // document.addEventListener("DOMContentLoaded", () => { // initializeDocsSearch(); // }); export { toggleSidebar, initializeDocsSearch }; Alpine.data("docs", () => ({ toggleSidebar, initializeDocsSearch, // Keep as-is })); ``` **Update template:** ```html ``` **✅ 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):** ```typescript 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):** ```html ``` **✅ 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):** ```typescript 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:** ```typescript 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:** ```html ``` **✅ 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):** ```typescript // 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): ```typescript // DELETE this entire block: // document.addEventListener("DOMContentLoaded", () => { // initDragAndDrop(); // ... all 70+ lines ... // }); ``` **Add to export statement:** ```typescript export { closeDashboardSettings, openDashboardSettings, initDashboard, // ← ADD THIS saveDashboardSettings, scrollCarousel, // ... keep all other exports ... }; ``` **Add to Alpine.data:** ```typescript 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: ```html ``` ### Step 3.5: Verify All TypeScript Files ```bash # Build all TypeScript npm run build:ts # Should succeed with 0 errors ``` --- ## Step 4: Regenerate Templates ```bash # 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 ```bash # 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:** ```bash 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