Files
bookhoard/COLLECTIONS_CLEANUP_GUIDE.md
T
john-okeefe ab2e2427cc docs(ssr): create SSR-first Alpine.js guide and update cleanup guide
Created comprehensive SSR_FIRST_ALPINE_GUIDE.md to establish SSR-first
architecture principles for Alpine.js integration.

New Guide: SSR_FIRST_ALPINE_GUIDE.md

Covers:
- SSR-first principles (state in templates, no fetch in x-init for SSR pages)
- Three page type classifications:
  * Type 1: 80% SSR (Collections, Conflicts) - backend provides all data
  * Type 2: SSR + Interactive (Dashboard, Admin Library) - SSR + interactivity
  * Type 3: 80% JavaScript (Analytics) - x-init fetches all data (intentional)
- The SSR data fetch problem (x-init replacing SSR content)
- DOMContentLoaded cleanup strategies
- Page-by-page strategy for each type
- Authentication & SSR (server-side token injection)
- Verification checklist and testing approach
- Architecture diagram showing data flow

Key Principles:
-  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
-  State lives in template (x-data), not TypeScript

Updated: COLLECTIONS_CLEANUP_GUIDE.md

Changes:
- Added reference to SSR_FIRST_ALPINE_GUIDE.md as authority
- Removed two-option approach (no more choices)
- Documented that admin library SSR bug is already fixed (commit 1b9bc64)
- Simplified dashboard approach (wrap existing code in initDashboard)
- Simplified docs approach (simple setup, no data fetch)
- Updated summary to reflect completed work
- Added architecture section showing state location

Architecture Clarity:
- Templates: UI state (x-data, x-show)
- Backend: SSR data
- TypeScript: Business logic only
- No hybrid approach - follow SSR-first principles

References:
- SSR_FIRST_ALPINE_GUIDE.md - Complete SSR-first architecture
- ALPINE_COMPLETION_GUIDE.md - Full Alpine.js migration (future goal)
- PROJECT_GUIDELINES.md - Project standards

This establishes a single source of truth for SSR-first Alpine.js
architecture and removes confusion about which approach to use.
2026-03-12 18:07:16 -04:00

710 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<!-- 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 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
<!-- templates/docs.templ -->
<body x-data="docs" x-init="initializeDocsSearch" class="theme-{ user.Theme }">
```
**✅ 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
<body x-data="library" x-init="initializeLibraryAdmin" class="theme-{ user.Theme }">
```
**✅ 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
<!-- templates/dashboard.templ line 19 -->
<body x-data="dashboard" x-init="initDashboard" class="theme-{ user.Theme }">
```
**✅ 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
<!-- 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
- ✅ **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