Files
bookhoard/TYPESCRIPT_CONVERSION_PLAN.md
T
john-okeefe 4d756acff4 docs(typescript): add TypeScript conversion plan for inline JavaScript
- 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
2026-02-17 21:58:33 -05:00

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:

  1. Convert web/static/search.jsweb/ts/features/search/search.ts
  2. Add proper types for search results (match handlers.MediaItem)
  3. Extract keyboard navigation logic into pure functions
  4. 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:

  1. Create web/ts/core/storage.ts

    • localStorage wrapper with type safety
    • Token management helpers
    • Theme persistence
  2. Create web/ts/core/dom.ts

    • escapeHtml utility
    • querySelector wrappers with null checks
    • Element creation helpers
  3. Create web/ts/core/api.ts

    • API client wrapper
    • Automatic auth header injection
    • Error handling integration with toast system
  4. 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}/rules endpoints
  • 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-action attributes 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.json with 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('&lt;script&gt;');
    });
});

// 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:

  1. Open DevTools
  2. Settings → Disable JavaScript
  3. Reload page
  4. 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 action and method attributes
  • 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 tsc setup, 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:

  1. Git revert TypeScript files
  2. Inline JavaScript still works (wasn't deleted yet)
  3. Fix issues and retry
  4. 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

  1. Review and approve this plan
  2. Setup Phase 1: Convert search.js → TypeScript
  3. Begin Phase 2: Create core utility modules
  4. Iterate through phases 3-5 (feature conversion)
  5. 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