# Alpine.js and SSR Architecture Cleanup Guide ## Overview This guide provides a systematic, page-by-page approach to fixing Alpine.js issues while maintaining SSR-first architecture. ### 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 Alpine.js Strategy **Different pages have different SSR/JavaScript ratios:** - **Analytics Page (80% JavaScript):** Dynamic charts/data that fetch on page load - ✅ `x-init="loadAnalytics"` fetches data and replaces content - ✅ This is intentional - analytics is a JavaScript-heavy page - **Dashboard Page (SSR + Interactive):** SSR for speed, JS for drag-drop/library switching - ✅ SSR provides initial dashboard data - ✅ x-init ONLY sets up event listeners (drag-drop, modals) - ✅ x-init does NOT fetch data (data already SSR'd) - ✅ `switchLibrary()` fetches only when user changes library dropdown - **Admin Library Page (SSR + Modals/CRUD):** SSR list, JS for management - ✅ SSR provides initial library list - ✅ x-init ONLY sets up event listeners and modals - ❌ BUG: `initializeLibraryAdmin()` was calling `reloadLibraries()` which fetches data - ❌ This replaces the SSR content on page load - MUST FIX - **Most Pages (80% SSR):** Server-rendered with interactive areas - ✅ Use x-init for setup only (event listeners, modals) - ❌ NEVER fetch data in x-init if data is already SSR'd **Key Principle:** - ✅ DO in x-init: Set up event listeners, initialize modals, setup drag-drop - ❌ DON'T in x-init: Fetch data via API, replace SSR innerHTML ### Review Status **Pages methodically reviewed so far:** - ✅ **Dashboard** - Completed, working correctly - 🔄 **Collections** - In progress (fixes in progress) - ⏸️ **Admin Library** - Identified bug, fix documented in this guide - ⏸️ **Other pages** - Not yet reviewed, will be done page-by-page **Goal:** Clean, working Alpine.js integration with proper 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, NO DATA FETCH **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(); }); export { toggleSidebar, initializeDocsSearch }; Alpine.data("docs", () => ({ toggleSidebar, initializeSearch: initializeDocsSearch, })); ``` **TWO OPTIONS:** --- ### **OPTION A: Use x-init (Simple)** **Remove DOMContentLoaded:** ```typescript // DELETE: // document.addEventListener("DOMContentLoaded", () => { // initializeDocsSearch(); // }); export { toggleSidebar, initializeDocsSearch }; Alpine.data("docs", () => ({ toggleSidebar, initializeDocsSearch, // Keep as-is })); ``` **Update template:** ```html ``` **✅ Simple** **✅ Clear initialization** --- ### **OPTION B: Event Delegation Pattern (See ALPINE_COMPLETION_GUIDE.md)** **If the search input uses data-action attributes, use global event delegation:** **Remove DOMContentLoaded and initializeDocsSearch entirely:** ```typescript // DELETE both: // document.addEventListener("DOMContentLoaded", () => { // initializeDocsSearch(); // }); // function initializeDocsSearch() { /* ... */ } // Export only business logic export { toggleSidebar }; // Keep if used // Search setup via global event delegation in main.ts or separate file ``` **Template:** ```html ``` **✅ No initialization** **✅ Works with existing event delegation** --- ### **Recommendation: OPTION A** Since `initializeDocsSearch()` doesn't fetch data and only sets up a listener, using x-init is fine and keeps the code clear. ### Step 3.3: library.ts - TWO APPROACHES **⚠️ CRITICAL SSR BUG:** The `initializeLibraryAdmin()` function currently calls `reloadLibraries()` which fetches data from the API and replaces the SSR-rendered library list. **Reference:** The full Alpine.js reactive pattern is documented in `ALPINE_COMPLETION_GUIDE.md` **File:** `web/src/library.ts` **Current Problem (around line 653-655):** ```typescript function initializeLibraryAdmin(): void { // Setup event listeners const librariesList = document.getElementById("libraries-list"); if (librariesList) { librariesList.addEventListener("click", handleLibraryListClick); } // ... more setup ... // ❌ BUG: This fetches data and replaces SSR content on page load! void reloadLibraries(); } ``` **TWO SOLUTIONS - Choose one:** --- ### **OPTION A: Minimal Fix (Quick) - Remove Data Fetch from Init** **Fix library.ts:** ```typescript function initializeLibraryAdmin(): void { // Setup event listeners const librariesList = document.getElementById("libraries-list"); if (librariesList) { librariesList.addEventListener("click", handleLibraryListClick); } // ... keep all existing setup code ... // ✅ REMOVED: void reloadLibraries(); // SSR provides initial library list - no need to fetch on page load // reloadLibraries() is still available to call AFTER create/delete/update operations } ``` **Template remains:** ```html ``` **✅ Keeps current architecture** **✅ Minimal code changes** **⚠️ Still uses manual event listeners** --- ### **OPTION B: Full Alpine.js Reactive Pattern (Recommended) - See ALPINE_COMPLETION_GUIDE.md** **This approach eliminates ALL manual DOM manipulation and event listeners.** **Step 1: Update template to use reactive state:** **File:** `templates/admin_library.templ` **Find modals (around lines 93-155):** **Current (create library modal):** ```html ``` **Change to:** ```html
``` **Update button to open modal (line 22):** ```html ``` **Step 2: Remove manual DOM manipulation from library.ts:** **Delete these functions (no longer needed):** ```typescript // DELETE ALL: function showCreateLibraryModal() { /* ... */ } function hideCreateLibraryModal() { /* ... */ } function showDeleteModal() { /* ... */ } function hideDeleteModal() { /* ... */ } function showFolderBrowser() { /* ... */ } function hideFolderBrowser() { /* ... */ } // DELETE initializeLibraryAdmin entirely: function initializeLibraryAdmin() { /* ... */ } ``` **Keep ONLY business logic functions:** ```typescript // KEEP - These are pure business logic, no DOM manipulation: async function handleCreateLibrarySubmit(event: Event) { /* ... */ } async function deleteLibrary(libraryId: string) { /* ... */ } async function addLibraryFolder(libraryId: string) { /* ... */ } async function removeLibraryFolder(libraryId: string, folderPath: string) { /* ... */ } async function showLibraryFolders(libraryId: string) { /* renders folders via innerHTML - keep for now */ } async function loadUserVisibility() { /* renders checkboxes via innerHTML - keep for now */ } async function setLibraryVisibility(userId: string, libraryId: string, isVisible: boolean) { /* ... */ } ``` **Step 3: Remove x-init from template:** **File:** `templates/admin_library.templ` (line 11) ```html ``` **Step 4: Update Alpine.data registration:** **File:** `web/src/library.ts` (bottom of file) ```typescript // BEFORE: Alpine.data("library", () => ({ addLibraryFolder, confirmDeleteLibrary, deleteLibrary, editLibrary, handleCreateLibrarySubmit, hideDeleteModal, hideFolderBrowser, initializeLibraryAdmin, // ← Remove this loadUserVisibility, navigateFolderBrowser, removeLibraryFolder, selectBrowseFolder, setLibraryVisibility, showDeleteModal, showFolderBrowser, showLibraryFolders, })); // AFTER: Alpine.data("library", () => ({ // Only business logic functions addLibraryFolder, confirmDeleteLibrary, deleteLibrary, editLibrary, handleCreateLibrarySubmit, loadUserVisibility, navigateFolderBrowser, removeLibraryFolder, selectBrowseFolder, setLibraryVisibility, showLibraryFolders, })); ``` **✅ Eliminates ALL manual DOM manipulation** **✅ No initialization needed** **✅ Reactive state in template** **✅ Follows full Alpine.js pattern from ALPINE_COMPLETION_GUIDE.md** --- ### **Which Option to Choose?** **Choose Option A (Minimal Fix) if:** - You want to fix the SSR bug quickly - You're comfortable with manual event listeners - You plan to refactor to full Alpine.js later **Choose Option B (Full Alpine.js) if:** - You want the cleanest architecture - You want to eliminate all manual DOM manipulation - You're following the pattern in ALPINE_COMPLETION_GUIDE.md - You want smooth transitions and better UX **Recommendation:** **Option B** - Full Alpine.js reactive pattern. It's cleaner, more maintainable, and aligns with the long-term architecture goal. ### Step 3.4: dashboard.ts - EVENT DELEGATION ALREADY EXISTS **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(); // ← Drag-drop initialization 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; // ... more cases ... } }); // ... library select setup ... }); ``` **TWO OPTIONS:** --- ### **OPTION A: Wrap in initDashboard() function** **Create wrapper function at end of dashboard.ts:** ```typescript function initDashboard() { initDragAndDrop(); document.addEventListener("click", (e: Event) => { // ... existing click handler code ... }); document.addEventListener("input", (e: Event) => { // ... existing input handler code ... }); // ... library select and localStorage code ... } export { initDashboard }; Alpine.data("dashboard", () => ({ initDashboard, })); ``` **Update template:** ```html ``` **✅ Keeps current event delegation pattern** **✅ Simple wrapper function** **⚠️ Still runs on page load** --- ### **OPTION B: Remove DOMContentLoaded, Rely on Global Event Delegation** **Since dashboard already uses data-action attributes with global listeners, we can remove DOMContentLoaded entirely:** **Remove from dashboard.ts (lines 494-521):** ```typescript // DELETE this entire block: document.addEventListener("DOMContentLoaded", () => { // ... all of it ... }); ``` **Move drag-drop initialization to separate function (can be called later if needed):** ```typescript // Drag-drop requires setup after content loads // Since dashboard content is SSR'd, we can initialize immediately function initDragAndDrop() { // ... existing drag-drop code ... } // Call it directly (no need to wait for DOMContentLoaded) initDragAndDrop(); // Global event listeners are already set up in main.ts or run immediately document.addEventListener("click", (e: Event) => { // ... existing click handler ... }); document.addEventListener("input", (e: Event) => { // ... existing input handler ... }); // ... library select setup ... ``` **No template changes needed:** ```html ``` **✅ No initialization on page load** **✅ Relies on existing event delegation** **✅ SSR-first compatible** **⚠️ Drag-drop initializes immediately (might be too early)** --- ### **Recommendation: OPTION A** Use the wrapper function approach. The dashboard has complex initialization (drag-drop, localStorage check) that benefits from running after Alpine loads. The event delegation pattern is already correct - just wrap it properly. **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 - 🔄 **Collections** - In progress (fixes in progress) - ⏸️ **Admin Library** - SSR bug identified, TWO fix approaches documented - ⏸️ **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 **Template Files (1 file):** - `templates/analytics.templ` - ✅ Added x-init="loadAnalytics" (JS-heavy page, correct) ### Two Approaches Available **This guide now presents TWO approaches for each page:** **OPTION A: Minimal Fix (Quick)** - Fix SSR bugs by removing data fetch from init functions - Keep x-init for setup only - Keep current event listener patterns - Good for quick fixes **OPTION B: Full Alpine.js Reactive Pattern (Recommended - see ALPINE_COMPLETION_GUIDE.md)** - Eliminate ALL manual DOM manipulation - Use x-data for state, x-show for visibility - Use @click.outside for closing dropdowns/modals - Use x-transition for smooth animations - No initialization functions needed - Aligns with long-term architecture **Reference:** `ALPINE_COMPLETION_GUIDE.md` contains the complete Alpine.js pattern with examples ### Pending Fixes **Admin Library (Step 3.3) - TWO approaches:** - **Option A:** Remove `reloadLibraries()` from `initializeLibraryAdmin()` - Quick fix - **Option B:** Full Alpine.js reactive pattern - Eliminate all manual DOM manipulation - Template already has `x-data="library" x-init="initializeLibraryAdmin"` **Dashboard (Step 3.4):** - **Option A:** Wrap existing code in `initDashboard()` function - **Option B:** Remove DOMContentLoaded, rely on event delegation - Already uses data-action event delegation correctly **Docs (Step 3.2):** - **Option A:** Use x-init for search setup (simple, no data fetch) - **Option B:** Use event delegation pattern ### 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 ✅ **Analytics (80% JS):** x-init fetches data - intentional for dynamic page ✅ **Dashboard (SSR + JS):** x-init ONLY sets up event listeners, no data fetch ✅ **Admin Library:** Choose Option A (quick) or Option B (full Alpine.js) ✅ **Collections:** SSR provides data, JS for interactivity (in progress) **Key Principle:** - ✅ DO in x-init: Set up event listeners, initialize modals, setup drag-drop - ❌ DON'T in x-init: Fetch data via API, replace SSR innerHTML ### Architecture Decision Point **You're at a decision point:** **Path 1: Quick Fixes (Option A for all pages)** - Fix SSR bugs by removing data fetch from inits - Keep current event listener patterns - Faster to complete - Can refactor to full Alpine.js later **Path 2: Full Alpine.js Migration (Option B for all pages)** - Follow ALPINE_COMPLETION_GUIDE.md pattern - Eliminate all manual DOM manipulation - Cleaner long-term architecture - More work upfront, but better result **Recommendation:** Mix both approaches based on page complexity: - Simple pages (docs): Use Option A - Complex pages (admin library): Use Option B (full Alpine.js) - This lets you learn the pattern progressively ### Next Steps **Choose your approach:** **If Path 1 (Quick Fixes):** 1. Apply Option A to admin_library, dashboard, docs 2. Continue page-by-page review 3. Plan full Alpine.js migration for later **If Path 2 (Full Alpine.js):** 1. Follow ALPINE_COMPLETION_GUIDE.md completely 2. Start with header.templ (reference implementation) 3. Apply Alpine.store pattern to all modals 4. Eliminate all manual DOM manipulation **If Mixed Approach (Recommended):** 1. Apply Option B (full Alpine.js) to admin_library 2. Apply Option A (quick fix) to dashboard and docs 3. Learn the pattern as you go 4. Gradually migrate to full Alpine.js **Continue Page-by-Page Review:** - Complete collections page fixes - Review and fix remaining pages one at a time - Update this guide as each page is completed