jedmund-svelte/docs/task-3-project-form-refactor-plan.md
Justin Edmund 0c5e9c8d13 docs: add Task 3 project form refactor plan
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>
2025-10-07 22:41:11 -07:00

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.svelte
    • ProjectBrandingForm.svelte
    • ProjectImagesForm.svelte
    • ProjectStylingForm.svelte
    • ProjectGalleryForm.svelte
  • Autosave integrated: Uses createAutoSaveStore from 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 $effect blocks (~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

  1. Not reusable: Same patterns will be copy-pasted to PostForm, EssayForm, etc.
  2. Hard to test: Logic is tightly coupled to component lifecycle
  3. Unclear boundaries: Business logic mixed with UI orchestration
  4. 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

  1. Create src/lib/stores/project-form.svelte.ts
  2. Extract state, validation, and field mutation logic
  3. Add unit tests for store
  4. Export TypeScript types

Phase 2: Create Reusable Helpers

  1. Create src/lib/admin/useDraftRecovery.svelte.ts
  2. Create src/lib/admin/useFormGuards.svelte.ts
  3. Document usage patterns

Phase 3: Refactor ProjectForm

  1. Update ProjectForm.svelte to use new store and helpers
  2. Remove duplicated logic
  3. Test create/edit flows
  4. Test autosave, draft recovery, navigation guards

Phase 4: Extract Draft Prompt UI

  1. Create DraftPrompt.svelte component
  2. Update ProjectForm to use it
  3. Will be reusable by other forms

Testing Strategy

Unit Tests

  • project-form.svelte.ts: Field updates, validation, payload building
  • useDraftRecovery.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:

  1. Apply to PostForm (essays, posts)
  2. Apply to MediaForm (photo editing)
  3. Extract common form shell (header, tabs, actions) into FormShell.svelte
  4. 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