Document the planned refactoring of ProjectForm.svelte to use: - Store factory for form state management - Reusable draft recovery helper - Reusable form guards helper - Simplified component structure This will reduce ProjectForm from ~719 lines to ~200-300 lines and establish patterns for PostForm, EssayForm, and other admin forms. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
7.9 KiB
7.9 KiB
Task 3: Project Form Modularization & Store Extraction
Overview
Refactor ProjectForm.svelte (currently ~719 lines) to use composable stores and reusable helpers, reducing duplication and improving testability.
Current State Analysis
✅ Already Modularized
- Section components exist:
ProjectMetadataForm.svelteProjectBrandingForm.svelteProjectImagesForm.svelteProjectStylingForm.svelteProjectGalleryForm.svelte
- Autosave integrated: Uses
createAutoSaveStorefrom Task 6
❌ Needs Extraction
- No store abstraction: All form state lives directly in the component (~50 lines of state declarations)
- Draft recovery scattered: Manual logic spread across multiple
$effectblocks (~80 lines) - Navigation guards duplicated:
beforeNavigate,beforeunload, Cmd+S shortcuts (~90 lines total) - Form lifecycle boilerplate: Initial load, populate, validation (~60 lines)
Issues with Current Approach
- Not reusable: Same patterns will be copy-pasted to PostForm, EssayForm, etc.
- Hard to test: Logic is tightly coupled to component lifecycle
- Unclear boundaries: Business logic mixed with UI orchestration
- Maintenance burden: Bug fixes need to be applied to multiple forms
Proposed Architecture
1. Create Store Factory: src/lib/stores/project-form.svelte.ts
Purpose: Centralize form state management and validation logic.
API Design:
export function createProjectFormStore(project?: Project) {
// Internal state
const fields = $state<ProjectFormData>({ ...defaultProjectFormData })
const validationErrors = $state<Record<string, string>>({})
const isDirty = $derived(/* compare fields to original */)
return {
// Read-only derived state
fields: readonly fields,
validationErrors: readonly validationErrors,
isDirty,
// Actions
setField(key: keyof ProjectFormData, value: any): void
setFields(data: Partial<ProjectFormData>): void
validate(): boolean
reset(): void
populateFromProject(project: Project): void
buildPayload(): ProjectPayload
}
}
export type ProjectFormStore = ReturnType<typeof createProjectFormStore>
Benefits:
- Type-safe field access with autocomplete
- Centralized validation logic
- Easy to unit test
- Can be used standalone (e.g., in tests, other components)
2. Create Draft Recovery Helper: src/lib/admin/useDraftRecovery.svelte.ts
Purpose: Extract draft restore prompt logic for reuse across all forms.
API Design:
export function useDraftRecovery<TPayload>(options: {
draftKey: string | null
onRestore: (payload: TPayload) => void
enabled?: boolean
}) {
const showPrompt = $state(false)
const draftTimestamp = $state<number | null>(null)
const timeTicker = $state(0)
const draftTimeText = $derived.by(() =>
draftTimestamp ? (timeTicker, timeAgo(draftTimestamp)) : null
)
// Auto-detect draft on mount
$effect(() => { /* ... */ })
// Update time display every minute
$effect(() => { /* ... */ })
return {
showPrompt: readonly showPrompt,
draftTimeText,
restore(): void
dismiss(): void
}
}
Usage:
<script>
const draftRecovery = useDraftRecovery({
draftKey: draftKey,
onRestore: (payload) => formStore.setFields(payload)
})
</script>
{#if draftRecovery.showPrompt}
<DraftPrompt
timeAgo={draftRecovery.draftTimeText}
onRestore={draftRecovery.restore}
onDismiss={draftRecovery.dismiss}
/>
{/if}
Benefits:
- Reusable across ProjectForm, PostForm, EssayForm, etc.
- Encapsulates timing and state management
- Easy to test in isolation
3. Create Form Guards Helper: src/lib/admin/useFormGuards.svelte.ts
Purpose: Extract navigation protection logic.
API Design:
export function useFormGuards(autoSave: AutoSaveStore | null) {
// Navigation guard: flush before route change
beforeNavigate(async (navigation) => { /* ... */ })
// Browser close warning
$effect(() => { /* addEventListener('beforeunload') */ })
// Cmd/Ctrl+S shortcut
$effect(() => { /* addEventListener('keydown') */ })
// No return value - purely side effects
}
Usage:
<script>
useFormGuards(autoSave)
</script>
Benefits:
- Single source of truth for form protection
- Consistent UX across all forms
- Easier to update behavior globally
4. Simplify ProjectForm.svelte
Before: ~719 lines After: ~200-300 lines
New structure:
<script lang="ts">
import { createProjectFormStore } from '$lib/stores/project-form.svelte'
import { useDraftRecovery } from '$lib/admin/useDraftRecovery.svelte'
import { useFormGuards } from '$lib/admin/useFormGuards.svelte'
import { createAutoSaveStore } from '$lib/admin/autoSave.svelte'
// Props
let { project = null, mode }: Props = $props()
// Create store
const formStore = createProjectFormStore(project)
// Autosave
const autoSave = mode === 'edit'
? createAutoSaveStore({ /* ... */ })
: null
// Draft recovery
const draftRecovery = useDraftRecovery({
draftKey: makeDraftKey('project', project?.id),
onRestore: (payload) => formStore.setFields(payload)
})
// Guards (navigation, beforeunload, Cmd+S)
useFormGuards(autoSave)
// UI state
let activeTab = $state('metadata')
// Trigger autosave on changes
$effect(() => {
formStore.fields; activeTab
if (hasLoaded && autoSave) autoSave.schedule()
})
</script>
<AdminPage>
<!-- Header with save actions -->
<!-- Tab controls -->
{#if activeTab === 'metadata'}
<ProjectMetadataForm bind:formData={formStore.fields} />
{:else if activeTab === 'case-study'}
<Composer bind:content={formStore.fields.caseStudyContent} />
{/if}
</AdminPage>
Implementation Steps
Phase 1: Create Store Factory
- Create
src/lib/stores/project-form.svelte.ts - Extract state, validation, and field mutation logic
- Add unit tests for store
- Export TypeScript types
Phase 2: Create Reusable Helpers
- Create
src/lib/admin/useDraftRecovery.svelte.ts - Create
src/lib/admin/useFormGuards.svelte.ts - Document usage patterns
Phase 3: Refactor ProjectForm
- Update
ProjectForm.svelteto use new store and helpers - Remove duplicated logic
- Test create/edit flows
- Test autosave, draft recovery, navigation guards
Phase 4: Extract Draft Prompt UI
- Create
DraftPrompt.sveltecomponent - Update ProjectForm to use it
- Will be reusable by other forms
Testing Strategy
Unit Tests
project-form.svelte.ts: Field updates, validation, payload buildinguseDraftRecovery.svelte.ts: Draft detection, restore, dismiss- Can use Vitest for rune-based stores
Integration Tests
- Full form lifecycle: load → edit → save
- Draft recovery flow
- Navigation guard behavior
- Autosave coordination
Manual QA
- Create new project
- Edit existing project
- Restore from draft
- Navigate away with unsaved changes
- Browser refresh warning
- Cmd+S immediate save
Success Criteria
- ProjectForm.svelte reduced to <350 lines
- Store factory fully typed with generics
- Draft recovery reusable across forms
- Navigation guards work consistently
- All existing functionality preserved
- Unit tests pass
- Manual QA checklist completed
Future Work (Post-Task 3)
Once this pattern is proven with ProjectForm:
- Apply to PostForm (essays, posts)
- Apply to MediaForm (photo editing)
- Extract common form shell (header, tabs, actions) into
FormShell.svelte - Add form-level error boundaries for graceful failure handling
Dependencies
- ✅ Task 6 (Autosave Store) - already complete
- ✅ Existing section components - already built
- ⏳ Need to ensure TypeScript strict mode compliance