8.0 KiB
Form Submission Patterns
Overview
This document defines the standard patterns for handling form submissions, toast notifications, and modal behavior across ThrillWiki.
Core Principles
Separation of Concerns
- Forms handle UI, validation, and data collection
- Parent Pages handle submission logic and user feedback
- Submission Helpers handle database operations
Single Source of Truth
- Only parent pages show success toasts
- Forms should not assume submission outcomes
- Modal closing is controlled by parent after successful submission
Toast Notification Rules
✅ DO
Parent Pages Show Toasts
const handleParkSubmit = async (data: FormData) => {
try {
await submitParkCreation(data, user.id);
toast({
title: "Park Submitted",
description: "Your submission has been sent for review."
});
setIsModalOpen(false); // Close modal after success
} catch (error) {
// Error already handled by form via handleError utility
}
};
Use Correct Terminology
- ✅ "Submitted for review" (for new entities)
- ✅ "Edit submitted" (for updates)
- ❌ "Created" or "Updated" (implies immediate approval)
Conditional Toast in Forms (Only for standalone usage)
// Only show toast if NOT being called from a parent handler
if (!initialData?.id) {
toast.success('Designer submitted for review');
onCancel();
}
❌ DON'T
Forms Should NOT Show Success Toasts for Main Submissions
// ❌ WRONG - Form doesn't know if submission succeeded
const handleFormSubmit = async (data: FormData) => {
await onSubmit(data);
toast({
title: "Park Created", // ❌ Misleading terminology
description: "The new park has been created successfully."
});
};
Duplicate Toasts
// ❌ WRONG - Both form and parent showing toasts
// Form:
toast({ title: "Park Created" });
// Parent:
toast({ title: "Park Submitted" });
Modal Behavior
Expected Flow
- User fills form and clicks submit
- Form validates and calls
onSubmitprop - Parent page handles submission
- Parent shows appropriate toast
- Parent closes modal via
setIsModalOpen(false)
Common Issues
Issue: Modal doesn't close after submission Cause: Form is showing a toast that interferes with normal flow Solution: Remove form-level success toasts
Issue: User sees "Created" but item isn't visible Cause: Using wrong terminology - submissions go to moderation Solution: Use "Submitted for review" instead of "Created"
Form Component Template
export function EntityForm({ onSubmit, onCancel, initialData }: EntityFormProps) {
const { user } = useAuth();
const { register, handleSubmit, /* ... */ } = useForm({
// ... form config
});
return (
<form onSubmit={handleSubmit(async (data) => {
if (!user) {
toast.error('You must be logged in to submit');
return;
}
try {
await onSubmit(data);
// ⚠️ NO SUCCESS TOAST HERE - parent handles it
// Exception: Standalone forms not in modals can show toast
} catch (error: unknown) {
handleError(error, {
action: initialData?.id ? 'Update Entity' : 'Create Entity',
metadata: { entityName: data.name }
});
// ⚠️ CRITICAL: Re-throw so parent can handle modal state
throw error;
}
})}>
{/* Form fields */}
</form>
);
}
Parent Page Template
export function EntityListPage() {
const [isModalOpen, setIsModalOpen] = useState(false);
const handleEntitySubmit = async (data: FormData) => {
try {
const result = await submitEntityCreation(data, user.id);
// ✅ Parent shows success feedback
toast({
title: "Entity Submitted",
description: "Your submission has been sent for review."
});
// ✅ Parent closes modal
setIsModalOpen(false);
// ✅ Parent refreshes data
queryClient.invalidateQueries(['entities']);
} catch (error) {
// Form already showed error via handleError
// Parent can optionally add additional handling
console.error('Submission failed:', error);
}
};
return (
<>
<Button onClick={() => setIsModalOpen(true)}>
Add Entity
</Button>
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<EntityForm
onSubmit={handleEntitySubmit}
onCancel={() => setIsModalOpen(false)}
/>
</Dialog>
</>
);
}
Error Handling
⚠️ CRITICAL: Error Propagation Pattern
Forms MUST re-throw errors after logging them so parent components can respond appropriately (keep modals open, show additional context, etc.).
Forms MUST re-throw errors:
} catch (error: unknown) {
// Log error for debugging and show toast to user
handleError(error, {
action: 'Submit Park',
userId: user?.id,
metadata: { parkName: data.name }
});
// ⚠️ CRITICAL: Re-throw so parent can handle modal state
throw error;
}
Why Re-throw?
- Parent needs to know submission failed
- Modal should stay open so user can retry
- User can fix validation issues and resubmit
- Prevents "success" behavior on failures
- Maintains proper error flow through the app
Parent-Level Error Handling
const handleParkSubmit = async (data: FormData) => {
try {
await submitParkCreation(data, user.id);
toast.success('Park submitted for review');
setIsModalOpen(false); // Only close on success
} catch (error) {
// Error already toasted by form via handleError()
// Modal stays open automatically because we don't close it
// User can fix issues and retry
console.error('Submission failed:', error);
}
};
Expected Error Flow:
- User submits form →
onSubmit()called - Submission fails → Form catches error
- Form shows error toast via
handleError() - Form re-throws error to parent
- Parent's catch block executes
- Modal stays open (no
setIsModalOpen(false)) - User fixes issue and tries again
Common Mistake:
// ❌ WRONG - Error not re-thrown, parent never knows
} catch (error: unknown) {
handleError(error, { action: 'Submit' });
// Missing: throw error;
}
Current Implementation Status
✅ Correct Implementation
DesignerForm.tsx- Shows "Designer submitted for review" only when!initialData?.idOperatorForm.tsx- Shows "Operator submitted for review" only when!initialData?.idPropertyOwnerForm.tsx- Shows "Property owner submitted for review" only when!initialData?.idManufacturerForm.tsx- Shows "Manufacturer submitted for review" only when!initialData?.idRideModelForm.tsx- No toasts, parent handles everythingRideForm.tsx- Shows "Submission Sent" with conditional descriptionParkForm.tsx- Fixed to remove premature success toast
Parent Pages
Parks.tsx- Shows "Park Submitted" ✅Operators.tsx- Shows "Operator Submitted" ✅Designers.tsx- Shows "Designer Submitted" ✅Manufacturers.tsx- Shows "Manufacturer Submitted" ✅ParkDetail.tsx- Shows "Submission Sent" ✅
Testing Checklist
When implementing or updating a form:
- Form validates input correctly
- Form calls
onSubmitprop with clean data - Form only shows error toasts, not success toasts (unless standalone)
- Parent page shows appropriate success toast
- Success toast uses correct terminology ("submitted" not "created")
- Modal closes after successful submission
- User sees single toast, not duplicates
- Error handling provides actionable feedback
- Form can be used both in modals and standalone
Related Files
src/lib/errorHandler.ts- Error handling utilitiessrc/lib/entitySubmissionHelpers.ts- Submission logicsrc/hooks/use-toast.ts- Toast notification hooktests/e2e/submission/park-creation.spec.ts- E2E tests for submission flow