10 KiB
Alpine.js Migration Plan
Overview
Incremental migration of Astrolabe from vanilla JavaScript to Alpine.js for reactive UI management. Each phase is independently deployable and leaves the project fully functional.
Guiding Principles
- Each step is independently deployable - Project always works
- No big-bang rewrites - Small, focused changes
- Test after each step - Catch issues early
- Alpine + Vanilla coexist - No forced conversions
- SnippetStorage/DatasetStorage remain authoritative - Alpine is view layer only
- Migration only, no new features - Convert existing functionality without adding new UI features
Architecture Philosophy
┌─────────────────────┐
│ Alpine.js (7KB) │ ← Reactivity + UI bindings
└──────────┬──────────┘
│ calls
▼
┌─────────────────────┐
│ Storage Layer │ ← All business logic
│ - SnippetStorage │ (filtering, sorting, CRUD)
│ - DatasetStorage │
└─────────────────────┘
Clean separation:
- Alpine: Handles reactivity, DOM updates, user interactions
- Storage: Single source of truth for data logic
Phase 1: Snippet Panel ✅ COMPLETE
Status: Done
Files: index.html, src/js/snippet-manager.js, src/js/app.js
What Was Converted
- Snippet list rendering with
x-for - Search with
x-model(reactive filtering) - Sort controls with
@clickand:class - Selection highlighting with
:class - Ghost card (+ Create New Snippet)
What Stayed Vanilla
- SnippetStorage (localStorage operations)
- Editor integration
- Meta fields (name, comment)
- All CRUD business logic
Key Learnings
- Alpine store keeps minimal UI state (
currentSnippetId) - Storage layer does all filtering/sorting
- Alpine component is thin wrapper
- Automatic reactivity eliminates manual DOM updates
Phase 2: Dataset Manager Modal ✅ COMPLETE
Status: Done
Files: src/js/dataset-manager.js, index.html
What Was Converted
- Dataset list rendering with
x-fortemplate - Selection highlighting with
:classbinding to Alpine store - Empty state with
x-show - Click handlers with
@click
Note: Dataset modal did NOT have sort/search controls before migration, so none were added.
Implementation Approach
- Add Alpine store with
currentDatasetIdfor selection state - Create
datasetList()component as thin wrapper around existing logic - Move meta formatting logic from inline HTML strings to component methods
- Convert HTML to use Alpine directives
- Update
renderDatasetList()to trigger Alpine component refresh - Update
selectDataset()to update Alpine store instead of manual DOM manipulation
What Stays Vanilla
- DatasetStorage (IndexedDB operations)
- Dataset detail panel
- Preview table rendering
- Import/export logic
- All dataset form/edit functionality
Key Learnings
- Alpine component is very thin
- Most logic moved from inline HTML strings to component methods
- Net code increase: only +20 lines total
- Same pattern as Phase 1: minimal, focused conversion
- Don't add features during migration - only convert what exists
Phase 3: View Mode Toggle (Draft/Published) ✅ COMPLETE
Status: Done
Files: index.html, src/js/snippet-manager.js, src/js/app.js, src/js/config.js
What Was Converted
- View mode toggle buttons (Draft/Published) with
:classbinding and@clickhandlers - All references to
currentViewModeglobal variable now use Alpine store - Removed vanilla event listeners from app.js
- Removed
currentViewModeglobal variable from config.js
Implementation Approach
- Added
viewModeproperty to Alpine snippets store (default: 'draft') - Converted button HTML to use
:classbinding and@clickhandlers - Updated all references to
currentViewModeto useAlpine.store('snippets').viewMode - Simplified
updateViewModeUI()function (now only handles publish/revert button visibility) - Removed vanilla event listeners from app.js
What Stays Vanilla
- Editor integration logic
- Publish/discard actions
- Publish/revert button visibility logic (handled by
updateViewModeUI)
Key Learnings
- Alpine store provides clean reactive state for view mode
- Toggle button active states now automatically update via Alpine
:classbinding - All business logic references updated to use Alpine store instead of global variable
updateViewModeUIsimplified but still needed for publish/revert button management
Phase 4: Settings Modal ✅ COMPLETE
Status: Done
Files: src/js/user-settings.js, index.html, src/js/app.js
What Was Converted
- Settings modal form with all input controls using
x-model - Apply/Reset/Cancel buttons with
@clickhandlers - Computed
isDirtyproperty to enable/disable Apply button - Conditional custom date format field with
x-show - Slider value displays with
x-text - All form state management moved to Alpine component
Implementation Approach
- Created
settingsPanel()component inuser-settings.jswith form state tracking - Used
x-model(with.numbermodifier where needed) for all form inputs:- Theme selects
- Font size and render debounce sliders
- Tab size select
- Checkboxes (minimap, word wrap, line numbers)
- Date format and custom format input
- Added computed
isDirtygetter to compare current vs. original state - Added computed
showCustomDateFormatgetter for conditional field visibility - Moved all apply/reset/cancel logic into Alpine component methods
- Removed vanilla event listeners and old
loadSettingsIntoUI()/applySettings()functions
What Stays Vanilla
- Settings storage layer (getSettings, updateSettings, etc.)
- Settings validation logic (validateSetting)
- Editor option updates (applied from within Alpine component)
- Theme application logic (applied from within Alpine component)
- Modal open/close functions (simple ModalManager calls)
Key Learnings
- Alpine component handles all form state reactivity
isDirtycomputed property automatically enables/disables Apply buttonx-model.numbermodifier ensures numeric values for sliders and selectsx-showprovides clean conditional rendering for custom date format field- All business logic (validation, saving, applying) stays in existing functions
- Net code reduction: ~150 lines of manual DOM manipulation removed
Phase 5: Chart Builder Modal
Status: Planned
Files: src/js/chart-builder.js, index.html
What to Convert
Chart builder form with dataset selection, chart type, and field mappings.
Implementation Approach
- Create
chartBuilder()component with form state - Load datasets on init, populate dropdowns with
x-for - Track selected dataset and available fields
- Generate spec preview with computed property
- Enable/disable insert button based on validation
What Stays Vanilla
- Dataset field detection
- Type inference logic
- Spec generation utilities
Phase 6: Meta Fields (Name, Comment)
Status: Planned
Files: index.html, src/js/snippet-manager.js
What to Convert
Name and comment fields with auto-save functionality.
Implementation Approach
- Add
snippetNameandsnippetCommenttosnippetList()component - Use
x-modelwith debounced input handlers - Call
loadMetadata()when snippet selected - Auto-save on change with 500ms debounce
What Stays Vanilla
- SnippetStorage save operations
Phase 7: Panel Visibility Toggles
Status: Planned
Files: index.html, src/js/panel-manager.js
What to Convert
Toggle buttons for showing/hiding panels.
Implementation Approach
- Create Alpine store for UI state (panel visibility flags)
- Convert toggle buttons to use
:classand@click - Add
x-showto panels with transitions - Persist visibility state to localStorage
What Stays Vanilla
- Panel resizing logic
- Keyboard shortcuts
Phase 8: Toast Notifications (Optional)
Status: Planned
Files: src/js/config.js, index.html
What to Convert
Toast notification system with auto-dismiss.
Implementation Approach
- Create Alpine store for toast queue
- Render toasts with
x-forand transitions - Update
Toastutility to add items to Alpine store - Auto-dismiss with setTimeout
What Stays Vanilla
- Toast message generation
Recommended Order
- ✅ Phase 1: Snippet Panel - DONE
- ✅ Phase 2: Dataset Manager - DONE
- ✅ Phase 3: View Mode Toggle - DONE
- ✅ Phase 4: Settings Modal - DONE
- Phase 6: Meta Fields - Before Chart Builder (simpler)
- Phase 7: Panel Toggles - Quick win
- Phase 5: Chart Builder - More complex, save for when confident
- Phase 8: Toast Notifications - Optional polish
Emergency Rollback Plan
If a phase causes issues:
- Quick Fix: Comment out Alpine directives, uncomment vanilla JS
- Git Revert: Each phase is a separate commit
- Hybrid Mode: Alpine and vanilla can coexist - revert problematic sections only
Post-Migration
After all phases complete:
Code Cleanup
- Remove no-op functions
- Remove unused vanilla event listeners
- Clean up global state variables
- Update JSDoc comments
Documentation
- Update architecture.md
- Document Alpine components
- Add Alpine.js to dependencies list
- Update CLAUDE.md with Alpine patterns
Questions & Decisions Log
| Date | Phase | Question | Decision | Rationale |
|---|---|---|---|---|
| 2025-01-24 | 1 | Store snippets in Alpine or Storage? | Storage | Single source of truth, Alpine just views |
| 2025-01-24 | 1 | Keep old functions as stubs? | Yes | Backwards compatibility, easier rollback |
| 2025-01-24 | 2 | Add sort/search to dataset modal? | No | Migration only - don't add features that didn't exist |
| 2025-01-24 | 2 | How much net code increase is acceptable? | ~20 lines | Alpine boilerplate worth it for reactivity gains |
Success Metrics
Quantitative
- ~300+ lines of code removed overall
- No performance regression
- Zero increase in bug reports
- All tests passing
Qualitative
- Code is more readable
- New features easier to add
- Less manual DOM manipulation
- Clearer separation of concerns