- Create comprehensive plan to convert ~5,500 lines of inline JS to TypeScript - Hybrid SSR + TypeScript CRUD approach (keeps existing JSON API) - Event delegation pattern (no inline onclick handlers) - Shared infrastructure: apiClient, toast, event utilities - Procedural/imperative style (no OOP, classes, inheritance) - 6 phases, 16-21 day timeline - Preserves single API for all clients (web, mobile, plugins) - No new backend routes needed
29 KiB
TypeScript Conversion Plan for Bookhoard
Executive Summary
Convert ~5,500 lines of inline JavaScript across 13 template files into organized TypeScript modules while preserving the existing Hybrid SSR architecture. The web UI will remain a client of the JSON API, alongside Kobo, KOReader, and future plugins.
Core Strategy: Server-Side Rendering (SSR) for initial page loads + TypeScript for CRUD operations via existing JSON API.
Current Architecture Analysis
Already Converted to TypeScript ✅
| Source | Output | Lines | Purpose |
|---|---|---|---|
web/src/toast.ts |
toast.js |
226 | Toast notifications, HTMX/fetch interceptors |
web/src/theme.ts |
theme.js |
130 | Theme management, smooth scrolling |
web/src/header.ts |
header.js |
~100 | Header dropdowns, theme/user menus |
web/src/device-management.ts |
device-management.js |
~70 | Device token copy/regeneration |
Remaining Standalone JavaScript ❌
| File | Lines | Purpose |
|---|---|---|
web/static/search.js |
275 | Header search with keyboard navigation, debouncing |
Inline JavaScript in Templates (~5,500 lines)
| Template | Script Lines | Primary Functions |
|---|---|---|
collection_rules.templ |
~400 | Rule CRUD operations, testing |
unlinked_books.templ |
~650 | Book matching, linking, bulk operations |
collections.templ |
~500 | Bulk operations, filtering |
devices.templ |
~350 | Token regeneration, sync URL display |
bookshelf.templ |
~250 | Book viewing, pagination |
dashboard.templ |
~300 | Statistics, recent activity |
api_explorer.templ |
~200 | API testing, cURL generation |
admin_library.templ |
~250 | Admin library scan |
admin_profile.templ |
~150 | Admin profile management |
admin.templ |
~150 | Admin dashboard actions |
index.templ |
~100 | Landing page theme preview |
login.templ |
~100 | Login theme selection |
progress.templ |
~100 | Reading progress |
Total: ~5,500 lines of inline JavaScript to convert
Architecture: Hybrid SSR + TypeScript CRUD
Current Pattern (Already Working)
┌─────────────────────────────────────────────────────────────┐
│ User visits /collections │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 1. Go handler: GET /collections │
│ 2. Fetches data from CollectionService │
│ 3. Renders templates.Collection(user, collections) │
│ 4. Returns complete HTML page with data │
└─────────────────────────────────────────────────────────────┘
↓
[Page displays instantly with data]
↓
┌─────────────────────────────────────────────────────────────┐
│ 5. User clicks "Add Rule" button │
│ 6. TypeScript: fetch POST /api/collections/{id}/rules │
│ 7. Server returns JSON │
│ 8. TypeScript updates DOM │
└─────────────────────────────────────────────────────────────┘
What This Preserves
✅ Fast initial page loads - SSR with data
✅ Progressive enhancement - Works without JavaScript
✅ Single API - All clients use /api/* endpoints
✅ Service layer as source of truth - All handlers use same services
✅ No handler duplication - No new HTML endpoints needed
✅ Plugin ecosystem ready - Plugins use JSON API
Proposed TypeScript Structure
web/ts/
├── core/
│ ├── toast.ts # ✅ Already converted
│ ├── theme.ts # ✅ Already converted
│ ├── storage.ts # NEW: localStorage wrapper with types
│ ├── dom.ts # NEW: DOM utilities (escapeHtml, querySelector)
│ └── api.ts # NEW: API client wrapper with auth
│
├── features/
│ ├── search/
│ │ ├── search.ts # Convert from search.js
│ │ └── types.ts # Search result types (match Go handlers)
│ │
│ ├── collections/
│ │ ├── rules.ts # Convert from inline JS
│ │ ├── bulk.ts # Convert from inline JS
│ │ └── types.ts # Reuse handlers.Rule, handlers.Collection
│ │
│ ├── linking/
│ │ ├── matcher.ts # Convert from inline JS
│ │ ├── bulk-link.ts # Convert from inline JS
│ │ ├── manual-link.ts # Convert from inline JS
│ │ └── types.ts # Reuse handlers.BookMatch, handlers.Progress
│ │
│ ├── devices/
│ │ ├── token.ts # Convert from inline JS
│ │ └── types.ts # Reuse handlers.Device types
│ │
│ ├── bookshelf/
│ │ ├── display.ts # Convert from inline JS
│ │ ├── pagination.ts # Convert from inline JS
│ │ └── types.ts # Reuse handlers.MediaItem types
│ │
│ ├── api-explorer/
│ │ ├── request.ts # Convert from inline JS
│ │ ├── response.ts # Convert from inline JS
│ │ └── curl.ts # Convert from inline JS
│ │
│ └── admin/
│ ├── scan.ts # Convert from inline JS
│ └── stats.ts # Convert from inline JS
│
└── shared/
├── events.ts # Event delegation utilities
└── auth.ts # Token management helpers
Type Sharing Strategy
Principle: Reuse Go Handler Types
❌ DON'T DO THIS:
// Duplicating types from Go handlers
interface Collection {
id: string;
name: string;
// ...
}
✅ DO THIS:
// Use types that match Go handlers exactly
// These types are already defined in Go and returned by /api/*
interface MediaItem {
id: string;
title: string;
author?: string;
library_id: string;
library_type_name: 'ebooks' | 'comics' | 'manga';
cover_image_path?: string;
}
// From handlers.CollectionData
interface CollectionData {
ID: string;
Name: string;
Description: string;
Color: string;
Icon: string;
}
Why:
- Single source of truth (Go handlers)
- API returns these types as JSON
- TypeScript matches exactly what server provides
- No duplication, no drift
Conversion Phases
Phase 1: Complete Standalone File Conversion
Priority: High | Effort: 1 day | Dependencies: None
Tasks:
- Convert
web/static/search.js→web/ts/features/search/search.ts - Add proper types for search results (match
handlers.MediaItem) - Extract keyboard navigation logic into pure functions
- Update templates to use new TypeScript module
Deliverable: All standalone JavaScript converted to TypeScript
Files:
- Create:
web/ts/features/search/search.ts - Create:
web/ts/features/search/types.ts - Delete:
web/static/search.js - Update: Templates referencing search functions
Phase 2: Core Utilities (Shared Infrastructure)
Priority: High | Effort: 1-2 days | Dependencies: None
Tasks:
-
Create
web/ts/core/storage.ts- localStorage wrapper with type safety
- Token management helpers
- Theme persistence
-
Create
web/ts/core/dom.ts- escapeHtml utility
- querySelector wrappers with null checks
- Element creation helpers
-
Create
web/ts/core/api.ts- API client wrapper
- Automatic auth header injection
- Error handling integration with toast system
-
Create
web/ts/shared/events.ts- Event delegation helpers
- Data attribute selectors
- Common event handlers
Deliverable: Reusable utilities for all feature modules
Files:
- Create:
web/ts/core/storage.ts - Create:
web/ts/core/dom.ts - Create:
web/ts/core/api.ts - Create:
web/ts/shared/events.ts
Phase 3: Low Complexity Features (Search, Header, Admin)
Priority: Medium | Effort: 3-4 days | Dependencies: Phase 2
Tasks:
3.1 Search Module (from Phase 1)
- Extract search logic from
search.ts - Add debounced search with proper types
- Keyboard navigation state management
- Integration with
/api/media-items/search
3.2 Header Dropdowns
- Extract from
header.ts(already TS) - Add event delegation for dropdowns
- Theme switching logic
- User menu interactions
3.3 Admin Actions
- Quick scan trigger
- System stats display
- Admin profile updates
Deliverable: Search, header, and admin features in TypeScript
Files:
- Refine:
web/ts/features/search/search.ts - Update:
web/ts/features/search/types.ts - Create:
web/ts/features/admin/scan.ts - Create:
web/ts/features/admin/stats.ts - Update:
web/src/header.ts(add event delegation)
Phase 4: Medium Complexity Features (Collections, Bookshelf, Devices)
Priority: Medium | Effort: 4-5 days | Dependencies: Phase 2, 3
Tasks:
4.1 Collection Rules
- Convert rule CRUD from
collection_rules.templ - Use
/api/collections/{id}/rulesendpoints - Rule testing functionality
- Bulk rule operations
4.2 Bookshelf Display
- Convert from
bookshelf.templ - Pagination logic
- Library selection state
- Book viewing interactions
4.3 Device Management
- Convert from
devices.templ - Token regeneration
- Sync URL display
- Device registration
Deliverable: Collections, bookshelf, and devices features in TypeScript
Files:
- Create:
web/ts/features/collections/rules.ts - Create:
web/ts/features/collections/bulk.ts - Create:
web/ts/features/collections/types.ts - Create:
web/ts/features/bookshelf/display.ts - Create:
web/ts/features/bookshelf/pagination.ts - Create:
web/ts/features/devices/token.ts - Create:
web/ts/features/devices/types.ts
Phase 5: High Complexity Features (Linking, Bulk Operations)
Priority: Low | Effort: 5-6 days | Dependencies: Phase 4
Tasks:
5.1 Book Linking
- Convert matching logic from
unlinked_books.templ - Search and match functionality
- Manual link modal
- Bulk auto-link
- Bulk get suggestions
5.2 API Explorer
- Convert from
api_explorer.templ - Request/response display
- cURL generation
- History tracking
Deliverable: Book linking and API explorer in TypeScript
Files:
- Create:
web/ts/features/linking/matcher.ts - Create:
web/ts/features/linking/bulk-link.ts - Create:
web/ts/features/linking/manual-link.ts - Create:
web/ts/features/linking/types.ts - Create:
web/ts/features/api-explorer/request.ts - Create:
web/ts/features/api-explorer/response.ts - Create:
web/ts/features/api-explorer/curl.ts
Phase 6: Template Integration & Cleanup
Priority: High | Effort: 2-3 days | Dependencies: All phases
Tasks:
6.1 Update Templates
- Replace
<script>blocks with<script src="/static/[feature].js"> - Remove inline
onclick="..."handlers - Add
data-actionattributes for event delegation - Ensure progressive enhancement (pages work without JS)
6.2 Verify Progressive Enhancement
- Test all pages with JavaScript disabled
- Ensure forms submit with full page reload
- Verify critical paths work without JS
6.3 Build Configuration
- Update
tsconfig.jsonwith new file structure - Configure build process for all modules
- Add source maps for debugging
6.4 Testing
- Verify all features work with TypeScript
- Check keyboard navigation
- Test form submissions
- Validate error handling
Deliverable: Clean templates, all inline JavaScript replaced with TypeScript modules
Code Style Guidelines
1. Procedural/Imperative with Functional Techniques
✅ GOOD:
// Pure function, no classes
function createRuleItem(rule: Rule): HTMLElement {
const div = document.createElement('div');
div.className = 'rule-item';
div.innerHTML = renderRuleHTML(rule);
return div;
}
// Event delegation (one listener)
document.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
const deleteBtn = target.closest('[data-action="delete-rule"]');
if (deleteBtn) {
const ruleId = deleteBtn.dataset.ruleId;
deleteRule(ruleId);
}
});
❌ BAD:
// Class-based, OOP
class RuleManager {
private rules: Rule[] = [];
constructor() { ... }
addRule(rule: Rule) { ... }
}
2. Type Definitions
Reuse Go Handler Types:
// From internal/handlers/book_matching.go
interface UnlinkedBookData {
ProgressID: string;
DeviceID: string;
TitleFromDevice: string;
SHA256: string;
PotentialMatches: PotentialMatchData[];
}
interface PotentialMatchData {
MediaItemID: string;
Title: string;
Author: string;
Confidence: number;
}
Only Create Frontend-Specific Types When Necessary:
// OK: Frontend-specific state
type SearchState = {
query: string;
results: MediaItem[];
selectedIndex: number;
};
// OK: UI-specific config
type ToastType = 'error' | 'success' | 'info';
3. Error Handling
Integrate with Toast System:
async function deleteRule(ruleId: string): Promise<void> {
try {
const response = await apiClient.delete(`/api/collections/rules/${ruleId}`);
if (!response.ok) {
const error = await response.json();
window.showToast.error(error.message || 'Failed to delete rule');
return;
}
window.showToast.success('Rule deleted');
removeRuleFromDOM(ruleId);
} catch (error) {
window.showToast.error('Network error: Unable to connect to server');
console.error('Delete rule error:', error);
}
}
4. API Client Pattern
Centralized API Client:
// web/ts/core/api.ts
class APIClient {
private baseURL = '/api';
private getAuthHeader(): string {
const token = localStorage.getItem('token');
return token ? `Bearer ${token}` : '';
}
async get(url: string): Promise<Response> {
return fetch(`${this.baseURL}${url}`, {
headers: {
'Authorization': this.getAuthHeader(),
'Content-Type': 'application/json'
}
});
}
async post(url: string, data: unknown): Promise<Response> {
return fetch(`${this.baseURL}${url}`, {
method: 'POST',
headers: {
'Authorization': this.getAuthHeader(),
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
}
async delete(url: string): Promise<Response> {
return fetch(`${this.baseURL}${url}`, {
method: 'DELETE',
headers: {
'Authorization': this.getAuthHeader()
}
});
}
}
export const apiClient = new APIClient();
5. Event Delegation Pattern
Replace Inline Handlers:
<!-- ❌ DON'T: Inline onclick -->
<button onclick="deleteRule('{rule.id}')">Delete</button>
<!-- ✅ DO: Data attributes -->
<button data-action="delete-rule" data-rule-id="{rule.id}">Delete</button>
Handle with Event Delegation:
document.addEventListener('click', async (e) => {
const target = e.target as HTMLElement;
const deleteBtn = target.closest('[data-action="delete-rule"]');
if (deleteBtn) {
const ruleId = deleteBtn.dataset.ruleId;
if (ruleId && confirm('Are you sure you want to delete this rule?')) {
await deleteRule(ruleId);
}
}
});
6. Progressive Enhancement
Ensure Works Without JavaScript:
<!-- Form submits with full page reload if JS fails -->
<form action="/collections/{id}/rules" method="POST">
<input type="hidden" name="field" value="title">
<input type="hidden" name="operator" value="contains">
<input type="text" name="value" required>
<button type="submit">Add Rule</button>
</form>
<!-- TypeScript enhances with AJAX submit -->
<script src="/static/features/collections/rules.js"></script>
Build & Deployment
TypeScript Configuration
Update tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "none",
"lib": ["ES2020", "DOM"],
"outDir": "./web/static",
"rootDir": "./web/ts",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": false,
"sourceMap": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"resolveJsonModule": true
},
"include": [
"web/ts/**/*.ts",
"web/src/**/*.ts"
],
"exclude": [
"node_modules"
]
}
Build Scripts
Update package.json:
{
"scripts": {
"build:css": "tailwindcss -i ./web/static/input.css -o ./web/static/style.css --watch",
"build:css:prod": "tailwindcss -i ./web/static/input.css -o ./web/static/style.css --minify",
"build:ts": "tsc",
"build:ts:watch": "tsc --watch",
"build:ts:prod": "tsc --sourceMap false",
"build:all": "npm run build:ts && npm run build:css:prod",
"dev": "npm run build:ts:watch & npm run build:css"
}
}
Output Structure
Compiled JavaScript:
web/static/
├── core/
│ ├── toast.js # ✅ Already exists
│ ├── theme.js # ✅ Already exists
│ ├── header.js # ✅ Already exists
│ ├── device-management.js # ✅ Already exists
│ ├── storage.js # NEW
│ ├── dom.js # NEW
│ └── api.js # NEW
├── features/
│ ├── search/
│ │ └── search.js # NEW
│ ├── collections/
│ │ ├── rules.js # NEW
│ │ └── bulk.js # NEW
│ ├── linking/
│ │ ├── matcher.js # NEW
│ │ ├── bulk-link.js # NEW
│ │ └── manual-link.js # NEW
│ ├── bookshelf/
│ │ ├── display.js # NEW
│ │ └── pagination.js # NEW
│ ├── devices/
│ │ └── token.js # NEW
│ ├── api-explorer/
│ │ ├── request.js # NEW
│ │ ├── response.js # NEW
│ │ └── curl.js # NEW
│ └── admin/
│ ├── scan.js # NEW
│ └── stats.js # NEW
└── shared/
├── events.js # NEW
└── auth.js # NEW
Testing Strategy
1. Unit Testing (Optional)
Test pure functions that don't depend on DOM:
// Test utilities
describe('escapeHtml', () => {
it('should escape HTML entities', () => {
expect(escapeHtml('<script>')).to.equal('<script>');
});
});
// Test formatters
describe('formatConfidence', () => {
it('should format confidence as percentage', () => {
expect(formatConfidence(0.95)).to.equal('95%');
});
});
2. Integration Testing
Test API interactions:
describe('deleteRule', () => {
it('should call DELETE /api/collections/rules/{id}', async () => {
const fetchSpy = sinon.stub(global, 'fetch');
await deleteRule('rule-123');
expect(fetchSpy.calledWith('/api/collections/rules/rule-123', {
method: 'DELETE'
})).to.be.true;
});
});
3. Manual Testing Checklist
- All CRUD operations work (create, read, update, delete)
- Toast notifications display correctly
- Error handling works (network errors, 500 errors, 404 errors)
- Keyboard navigation works (search, modals)
- Forms submit correctly
- Bulk operations work
- Progressive enhancement (test with JS disabled)
- All pages load with SSR data
- No console errors
4. Browser Testing
Test in:
- Chrome/Edge (Chromium)
- Firefox
- Safari (if available)
- Mobile browsers (responsive testing)
Progressive Enhancement Verification
Test Without JavaScript
Disable JavaScript in browser:
- Open DevTools
- Settings → Disable JavaScript
- Reload page
- Verify critical functionality works
What Should Work:
- ✅ Page loads with SSR data
- ✅ Navigation links work (full page reload)
- ✅ Forms submit (full page reload, not AJAX)
- ✅ All data visible on initial load
What Won't Work (Expected):
- ❌ Dynamic updates without page reload
- ❌ Toast notifications
- ❌ Keyboard navigation
- ❌ Modal dialogs
- ❌ Auto-complete search
Mitigation:
- Ensure forms have proper
actionandmethodattributes - Provide submit buttons for all operations
- Include helpful error messages in HTML responses
Risk Mitigation
Potential Issues
1. Breaking Changes During Conversion
- Risk: Converting inline JS breaks functionality
- Mitigation: Convert incrementally, test each feature
- Rollback: Keep inline JS until TypeScript is verified
2. Type Mismatches with Go Handlers
- Risk: TypeScript types don't match JSON responses
- Mitigation: Use Go handler types as source of truth
- Validation: Test API responses match TypeScript types
3. Progressive Enhancement Regression
- Risk: Pages require JavaScript to function
- Mitigation: Test with JS disabled before/after
- Validation: Forms must have action/method attributes
4. Build Complexity
- Risk: TypeScript compilation becomes bottleneck
- Mitigation: Use existing
tscsetup, add watch mode - Validation: Build time under 5 seconds
5. Bundle Size
- Risk: Too much JavaScript loaded
- Mitigation: Split by feature, load per-page
- Validation: Monitor bundle sizes, code splitting
Rollback Strategy
If conversion fails:
- Git revert TypeScript files
- Inline JavaScript still works (wasn't deleted yet)
- Fix issues and retry
- Delete inline JS only after TypeScript verified
Per-feature rollback:
# Before converting feature
git branch backup-before-collections
# If collections conversion breaks
git checkout backup-before-collections -- templates/collections.templ
# Fix TypeScript and try again
Estimated Timeline
| Phase | Duration | Dependencies | Deliverables |
|---|---|---|---|
| Phase 1: Standalone Conversion | 1 day | None | Search converted to TS |
| Phase 2: Core Utilities | 1-2 days | None | Storage, DOM, API, events modules |
| Phase 3: Low Complexity | 3-4 days | Phase 2 | Search, header, admin in TS |
| Phase 4: Medium Complexity | 4-5 days | Phase 2, 3 | Collections, bookshelf, devices in TS |
| Phase 5: High Complexity | 5-6 days | Phase 4 | Linking, API explorer in TS |
| Phase 6: Integration & Cleanup | 2-3 days | All phases | Clean templates, verified functionality |
| Total | 16-21 days | Full TypeScript conversion |
Success Criteria
- All inline JavaScript removed from templates
- All features work with TypeScript
- TypeScript compilation passes with strict mode
- Progressive enhancement maintained (pages work without JS)
- No type
any(except for legacy API responses) - Event delegation replaces inline handlers
- Build process integrated into npm scripts
- Full manual testing completed
- No regression in functionality
- Bundle sizes monitored and optimized
- SSR still works for all initial page loads
- All CRUD operations use existing
/api/*endpoints
Next Steps
- Review and approve this plan
- Setup Phase 1: Convert
search.js→ TypeScript - Begin Phase 2: Create core utility modules
- Iterate through phases 3-5 (feature conversion)
- Phase 6: Final integration and cleanup
Appendix: Template Integration Examples
Before: Inline JavaScript
<!-- templates/collection_rules.templ -->
<script>
let collectionId = '{ collection.ID }';
function renderRules(rules) {
const container = document.getElementById('rules-container');
container.innerHTML = rules.map((rule, index) => `
<div class="rule-item">
<span>${rule.field}</span>
<button onclick="deleteRule('${rule.id}')">Delete</button>
</div>
`).join('');
}
function deleteRule(ruleId) {
fetch(`/api/collections/${collectionId}/rules/${ruleId}`, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
}).then(response => {
if (response.ok) {
loadRules();
showToast('Rule deleted');
}
});
}
function loadRules() {
fetch(`/api/collections/${collectionId}/rules`)
.then(response => response.json())
.then(rules => renderRules(rules));
}
loadRules();
</script>
After: TypeScript Module + Event Delegation
TypeScript Module:
// web/ts/features/collections/rules.ts
import { apiClient } from '../../core/api.js';
import { escapeHtml } from '../../core/dom.js';
interface Rule {
id: string;
field: string;
operator: string;
value: string;
}
function renderRules(rules: Rule[]): void {
const container = document.getElementById('rules-container');
if (!container) return;
container.innerHTML = rules.map(rule => `
<div class="rule-item" data-rule-id="${rule.id}">
<span>${escapeHtml(rule.field)}</span>
<button data-action="delete-rule" data-rule-id="${rule.id}">Delete</button>
</div>
`).join('');
}
async function loadRules(collectionId: string): Promise<void> {
try {
const response = await apiClient.get(`/collections/${collectionId}/rules`);
if (!response.ok) {
throw new Error('Failed to load rules');
}
const rules: Rule[] = await response.json();
renderRules(rules);
} catch (error) {
console.error('Load rules error:', error);
window.showToast.error('Failed to load rules');
}
}
async function deleteRule(collectionId: string, ruleId: string): Promise<void> {
if (!confirm('Are you sure you want to delete this rule?')) {
return;
}
try {
const response = await apiClient.delete(`/collections/${collectionId}/rules/${ruleId}`);
if (!response.ok) {
throw new Error('Failed to delete rule');
}
window.showToast.success('Rule deleted');
// Remove from DOM
const ruleElement = document.querySelector(`[data-rule-id="${ruleId}"]`);
ruleElement?.remove();
} catch (error) {
console.error('Delete rule error:', error);
window.showToast.error('Failed to delete rule');
}
}
// Event delegation for delete buttons
document.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
const deleteBtn = target.closest('[data-action="delete-rule"]');
if (deleteBtn) {
const ruleId = deleteBtn.dataset.ruleId;
const collectionId = deleteBtn.dataset.collectionId;
if (ruleId && collectionId) {
deleteRule(collectionId, ruleId);
}
}
});
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
const collectionId = document.body.dataset.collectionId;
if (collectionId) {
loadRules(collectionId);
}
});
Cleaned Template:
<!-- templates/collection_rules.templ -->
<!DOCTYPE html>
<html>
<head>
<script src="/static/htmx.min.js"></script>
<script src="/static/core/toast.js"></script>
<script src="/static/core/api.js"></script>
<script src="/static/features/collections/rules.js"></script>
</head>
<body data-collection-id="{ collection.ID }">
<div id="rules-container">
<!-- SSR: Initial rules rendered server-side -->
{ range .Rules }
<div class="rule-item" data-rule-id="{ .ID }">
<span>{ .Field }</span>
<button data-action="delete-rule"
data-rule-id="{ .ID }"
data-collection-id="{ collection.ID }">
Delete
</button>
</div>
{ end }
</div>
</body>
</html>
Benefits:
- ✅ No inline JavaScript
- ✅ Event delegation (one listener)
- ✅ Type-safe API calls
- ✅ Better error handling
- ✅ Reusable module
- ✅ SSR still works (initial rules in HTML)
- ✅ Progressive enhancement (form can submit without JS)
Generated: 2025-02-17 Follows: PROJECT_GUIDELINES.md Architecture: Hybrid SSR + TypeScript CRUD