Updated COLLECTIONS_CLEANUP_GUIDE.md to present TWO approaches for each page, giving flexibility for quick fixes vs full migration. Two Approaches Now Available: OPTION A: Minimal Fix (Quick) - Fix SSR bugs by removing data fetch from init functions - Keep x-init for setup only (event listeners, modals) - Keep current event listener patterns - Good for quick fixes OPTION B: Full Alpine.js Reactive Pattern (Recommended) - See ALPINE_COMPLETION_GUIDE.md for complete pattern - 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 Updates to Guide Sections: Step 3.2 (docs.ts): - Added Option A: Use x-init (simple) - Added Option B: Event delegation pattern - Recommendation: Option A (simple setup, no data fetch) Step 3.3 (library.ts): - Added Option A: Remove reloadLibraries() from init (quick) - Added Option B: Full Alpine.js reactive pattern - Shows how to eliminate manual DOM manipulation - Recommendation: Option B for cleanest architecture Step 3.4 (dashboard.ts): - Added Option A: Wrap in initDashboard() function - Added Option B: Remove DOMContentLoaded, use delegation - Notes event delegation already exists - Recommendation: Option A (keep current pattern) Updated Summary Section: - Added architecture decision point (Path 1 vs Path 2) - Documented mixed approach as recommended - Clear guidance on which approach to use when - References ALPINE_COMPLETION_GUIDE.md throughout This allows developer to choose approach based on: - Page complexity - Time constraints - Learning progression - Long-term architecture goals The guide is now flexible enough to support both quick fixes and full Alpine.js migration as the developer progresses through the app page-by-page.
1038 lines
27 KiB
Markdown
1038 lines
27 KiB
Markdown
# 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
|
||
<!-- 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:
|
||
|
||
```html
|
||
<!-- 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
|
||
|
||
```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
|
||
<!-- 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, 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
|
||
<!-- templates/docs.templ -->
|
||
<body x-data="docs" x-init="initializeDocsSearch" class="theme-{ user.Theme }">
|
||
```
|
||
|
||
**✅ 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
|
||
<!-- templates/docs.templ -->
|
||
<body class="theme-{ user.Theme }">
|
||
<!-- No x-init needed -->
|
||
```
|
||
|
||
**✅ 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
|
||
<body x-data="library" x-init="initializeLibraryAdmin" class="theme-{ user.Theme }">
|
||
```
|
||
|
||
**✅ 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
|
||
<div id="create-library-modal" class="hidden fixed inset-0 z-50">
|
||
<div class="card rounded-lg p-6">
|
||
<h2>Create Library</h2>
|
||
<button data-action="hide-create-modal">✕</button>
|
||
<form id="create-library-form">...</form>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
**Change to:**
|
||
```html
|
||
<!-- Wrap page in x-data with modal state -->
|
||
<div x-data="{
|
||
createLibraryModalOpen: false,
|
||
deleteLibraryModalOpen: false,
|
||
folderBrowserModalOpen: false
|
||
}">
|
||
|
||
<!-- Keep existing content, but update modal: -->
|
||
|
||
<div x-show="createLibraryModalOpen"
|
||
x-transition
|
||
@click.self="createLibraryModalOpen = false"
|
||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||
style="background-color: rgba(0, 0, 0, 0.7); display: none;">
|
||
<div @click.stop class="card rounded-lg p-6 w-full max-w-md mx-4">
|
||
<div class="flex justify-between items-center mb-6">
|
||
<h2 class="text-xl font-bold">Create Library</h2>
|
||
<button @click="createLibraryModalOpen = false" class="p-2">✕</button>
|
||
</div>
|
||
<form id="create-library-form">...</form>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
```
|
||
|
||
**Update button to open modal (line 22):**
|
||
```html
|
||
<!-- Before -->
|
||
<button data-action="show-create-modal">+ Create Library</button>
|
||
|
||
<!-- After -->
|
||
<button @click="createLibraryModalOpen = true">+ Create Library</button>
|
||
```
|
||
|
||
**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
|
||
<!-- BEFORE -->
|
||
<body x-data="library" x-init="initializeLibraryAdmin" class="theme-{ user.Theme }">
|
||
|
||
<!-- AFTER -->
|
||
<body class="theme-{ user.Theme }">
|
||
```
|
||
|
||
**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
|
||
<!-- templates/dashboard.templ line 19 -->
|
||
<body x-data="dashboard" x-init="initDashboard" class="theme-{ user.Theme }">
|
||
```
|
||
|
||
**✅ 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
|
||
<!-- templates/dashboard.templ line 19 -->
|
||
<body class="theme-{ user.Theme }">
|
||
<!-- No x-data or x-init needed -->
|
||
```
|
||
|
||
**✅ 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
|
||
<!-- 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
|
||
|
||
```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
|