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.
27 KiB
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:
- Dead Alpine.js exports (functions that don't exist)
- DOMContentLoaded listeners running on wrong pages
- Missing x-init calls in templates
- 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 callingreloadLibraries()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:
# Check current errors
cd /home/nymusicman/Code/bookhoard
npm run build:ts
# Should see errors about:
# - "addbooksToAdd is not defined"
# - "removebooksToAdd is not defined"
# - "toggleBookSelection is not defined"
# - etc.
Step 1: Fix collections.ts Alpine.data Export
File: web/src/collections.ts
Problem: Alpine.data exports functions that were deleted in commit 93710a1.
Action: Update the Alpine.data export to only include existing functions.
Step 1.1: Read Current Alpine.data Export
# Check what's currently exported
tail -50 web/src/collections.ts | grep -A25 "Alpine.data"
You'll see something like:
Alpine.data("collections", () => ({
addbooksToAdd, // ❌ Does NOT exist - deleted
backToCollections,
closeCollectionModal,
createRule,
deleteRule,
filterCollectionBooks,
filterIcons,
hideAddBooksModal,
initCollectionDetail, // ❌ Does NOT exist - deleted
initColorSelection, // ❌ Does NOT exist - deleted
initIconSelection, // ❌ Does NOT exist - deleted
loadCollectionRules,
loadCollections,
navigateToCollection,
populateIconGrid,
removeBook,
removebooksToAdd, // ❌ Does NOT exist - deleted
searchBooksForCollections,
selectColor,
selectIcon,
showAddBooksModal,
showAllIcons, // ❌ Does NOT exist - deleted
setupHTMXAuth,
testRule,
toggleBookForRemoval, // ❌ Does NOT exist - deleted
toggleBookSelection, // ❌ Does NOT exist - deleted
updateSelectedCount,
}));
Step 1.2: Read Current Export Statement
# Check the export statement at the end of the file
grep -A30 "^export {" web/src/collections.ts
You'll see similar dead exports.
Step 1.3: Verify Which Functions Actually Exist
# Search for function definitions
grep -n "^function\|^async function" web/src/collections.ts
Expected output (actual existing functions):
backToCollections✓closeCollectionModal✓createRule✓deleteRule✓filterCollectionBooks✓filterIcons✓hideAddBooksModal✓loadCollectionRules✓loadCollections✓navigateToCollection✓populateIconGrid✓removeBook✓searchBooksForCollections✓selectColor✓selectIcon✓showAddBooksModal✓setupHTMXAuth✓testRule✓updateSelectedCount✓
Step 1.4: Update the Export Statement
Line 407: Change the export statement to only include existing functions:
export {
backToCollections,
closeCollectionModal,
createRule,
deleteRule,
filterCollectionBooks,
filterIcons,
hideAddBooksModal,
loadCollectionRules,
loadCollections,
navigateToCollection,
populateIconGrid,
removeBook,
searchBooksForCollections,
selectColor,
selectIcon,
showAddBooksModal,
setupHTMXAuth,
testRule,
updateSelectedCount,
};
Step 1.5: Update Alpine.data Registration
Line 424: Update Alpine.data to match the export:
Alpine.data("collections", () => ({
backToCollections,
closeCollectionModal,
createRule,
deleteRule,
filterCollectionBooks,
filterIcons,
hideAddBooksModal,
loadCollectionRules,
loadCollections,
navigateToCollection,
populateIconGrid,
removeBook,
searchBooksForCollections,
selectColor,
selectIcon,
showAddBooksModal,
setupHTMXAuth,
testRule,
updateSelectedCount,
}));
Step 1.6: Verify the Fix
# Build TypeScript
npm run build:ts
# Should now succeed with 0 errors
Step 2: Fix Template Function Calls
File: templates/collections.templ
Problem: Template calls functions that no longer exist.
Step 2.1: Remove Dead Function Calls from Collection Detail Page
Line 226: Remove the removebooksToAdd call:
<!-- BEFORE -->
<button
id="bulk-remove-btn"
@click="removebooksToAdd"
disabled
class="btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
🗑️ Remove Selected
</button>
<!-- AFTER -->
<button
id="bulk-remove-btn"
disabled
class="btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
🗑️ Remove Selected
</button>
Line 229: Remove addbooksToAdd call:
<!-- BEFORE -->
<button @click="addbooksToAdd" class="btn-primary px-4 py-2 rounded-lg">
➕ Add Selected Books
</button>
<!-- AFTER -->
<button type="button" class="btn-primary px-4 py-2 rounded-lg" style="opacity: 0.5; cursor: not-allowed;" disabled>
➕ Add Selected Books
</button>
Step 2.2: Regenerate Templates
# Generate Go template files
templ generate
# Should see: Complete [updates=0 duration=~40ms]
Step 3: Fix Other TypeScript Files (Remove DOMContentLoaded)
For each file, we'll remove the DOMContentLoaded listener and add x-init to the template.
Step 3.1: analytics.ts
File: web/src/analytics.ts
Current:
export { loadAnalytics };
document.addEventListener("DOMContentLoaded", loadAnalytics);
Alpine.data("analytics", () => ({
loadAnalytics,
}));
Change to:
export { loadAnalytics };
// REMOVE this line:
// document.addEventListener("DOMContentLoaded", loadAnalytics);
Alpine.data("analytics", () => ({
loadAnalytics,
}));
Template Update: templates/analytics.templ
Line 14: Add x-init to body tag:
<!-- BEFORE -->
<body x-data="analytics" class="theme-{ user.Theme }">
<!-- AFTER -->
<body x-data="analytics" x-init="loadAnalytics" class="theme-{ user.Theme }">
Step 3.2: docs.ts - 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):
document.addEventListener("DOMContentLoaded", () => {
initializeDocsSearch();
});
export { toggleSidebar, initializeDocsSearch };
Alpine.data("docs", () => ({
toggleSidebar,
initializeSearch: initializeDocsSearch,
}));
TWO OPTIONS:
OPTION A: Use x-init (Simple)
Remove DOMContentLoaded:
// DELETE:
// document.addEventListener("DOMContentLoaded", () => {
// initializeDocsSearch();
// });
export { toggleSidebar, initializeDocsSearch };
Alpine.data("docs", () => ({
toggleSidebar,
initializeDocsSearch, // Keep as-is
}));
Update template:
<!-- 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:
// 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:
<!-- 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):
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:
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:
<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):
<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:
<!-- 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):
<!-- 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):
// DELETE ALL:
function showCreateLibraryModal() { /* ... */ }
function hideCreateLibraryModal() { /* ... */ }
function showDeleteModal() { /* ... */ }
function hideDeleteModal() { /* ... */ }
function showFolderBrowser() { /* ... */ }
function hideFolderBrowser() { /* ... */ }
// DELETE initializeLibraryAdmin entirely:
function initializeLibraryAdmin() { /* ... */ }
Keep ONLY business logic functions:
// 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)
<!-- 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)
// 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):
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:
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:
<!-- 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):
// DELETE this entire block:
document.addEventListener("DOMContentLoaded", () => {
// ... all of it ...
});
Move drag-drop initialization to separate function (can be called later if needed):
// 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:
<!-- 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):
// Wrapper function for dashboard initialization
function initDashboard() {
initDragAndDrop();
document.addEventListener("click", (e: Event) => {
const target = e.target as HTMLElement;
const actionElem = target.closest("[data-action]") as HTMLElement;
const action = actionElem?.getAttribute("data-action");
switch (action) {
case "scroll-carousel": {
const collectionId =
target.dataset.direction || actionElem?.dataset.direction || "0";
if (collectionId) scrollCarousel(collectionId, direction);
break;
}
// ... keep all existing cases ...
}
});
document.addEventListener("input", (e: Event) => {
const target = e.target as HTMLElement;
const actionElem = target.closest("[data-input-action]") as HTMLElement;
const action = actionElem?.getAttribute("data-input-action");
switch (action) {
case "update-items-count": {
const input = target as HTMLInputElement;
const displayTarget = input.getAttribute("target");
if (displayTarget) updateItemsCount(input, displayTarget);
break;
}
}
});
// Load saved library on page load
const librarySelect = document.getElementById(
"library-select",
) as HTMLSelectElement;
if (librarySelect) {
librarySelect.addEventListener("change", (e) => {
const target = e.target as HTMLSelectElement;
if (target.value) {
switchLibrary(target.value);
}
});
}
const savedLibrary = localStorage.getItem("selectedLibrary");
const currentLibrary = new URLSearchParams(window.location.search).get(
"library_id",
);
if (savedLibrary && savedLibrary !== currentLibrary) {
window.location.href = `/dashboard?library_id=${savedLibrary}`;
}
}
Remove the old DOMContentLoaded block (delete lines 494-521):
// DELETE this entire block:
// document.addEventListener("DOMContentLoaded", () => {
// initDragAndDrop();
// ... all 70+ lines ...
// });
Add to export statement:
export {
closeDashboardSettings,
openDashboardSettings,
initDashboard, // ← ADD THIS
saveDashboardSettings,
scrollCarousel,
// ... keep all other exports ...
};
Add to Alpine.data:
Alpine.data("dashboard", () => ({
closeDashboardSettings,
openDashboardSettings,
initDashboard, // ← ADD THIS
saveDashboardSettings,
scrollCarousel,
// ... keep all other exports ...
}));
Template Update: templates/dashboard.templ
Line 19: Add x-data and x-init to body tag:
<!-- BEFORE -->
<body class="theme-{ user.Theme }">
<!-- AFTER -->
<body class="theme-{ user.Theme }" x-data="dashboard" x-init="initDashboard()">
Step 3.5: Verify All TypeScript Files
# Build all TypeScript
npm run build:ts
# Should succeed with 0 errors
Step 4: Regenerate Templates
# Generate Go template files
templ generate
# Expected: Complete [updates=X duration=~40ms]
# where X is the number of templates modified
Step 5: Build and Verify
# Build the Go server
go build ./cmd/server
# If build succeeds, you're done!
# If build fails, check the error and fix accordingly
Step 6: Test in Browser
-
Start the server:
podman compose up -d -
Open browser and test:
- Navigate to
/collections- should work with no console errors - Navigate to
/analytics- should load analytics data - Navigate to
/dashboard- drag and drop should work - Navigate to
/docs- search should work - Navigate to
/admin/library- library management should work
- Navigate to
-
Check browser console:
- No "X is not defined" errors
- No "n.bind is not a function" errors
- No "initX is not defined" errors
Summary
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 exportsweb/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()frominitializeLibraryAdmin()- 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):
- Apply Option A to admin_library, dashboard, docs
- Continue page-by-page review
- Plan full Alpine.js migration for later
If Path 2 (Full Alpine.js):
- Follow ALPINE_COMPLETION_GUIDE.md completely
- Start with header.templ (reference implementation)
- Apply Alpine.store pattern to all modals
- Eliminate all manual DOM manipulation
If Mixed Approach (Recommended):
- Apply Option B (full Alpine.js) to admin_library
- Apply Option A (quick fix) to dashboard and docs
- Learn the pattern as you go
- 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