Form Edit Mode Guidelines
Overview
Forms should support both create and edit modes using the FormMode enum. This guide covers implementing edit functionality for existing forms.
Component Props
- Add props interface to form component:
typescript1interface FormNameProps { 2 entity?: SerializedEntity; // e.g., SerializedCar, SerializedProfessionalService 3 formMode: FormMode; 4}
- Import
FormModefrom@/components/Form/types/form.types - Import serialized entity type from appropriate types file (e.g.,
SerializedCarfrom@/lib/vehicles/cars/types/cars.types) - Component signature:
const FormName: FC<FormNameProps> = ({ entity, formMode }) => { ... }
Form Mode Detection
- Add mode check at the start of component:
typescript1const isCreateMode = formMode === FormMode.Create;
- Use
isCreateModeto conditionally render UI elements, change button text, or adjust validation
Required Imports for Edit Mode
FormModefrom@/components/Form/types/form.typesSerializedEntitytype from appropriate types fileExistingImageItemfrom@/lib/files/uploadFiles- Edit action function (e.g.,
editCarAdfrom@/lib/vehicles/cars/actions/editCarAd)
Existing Images State Management
- Initialize
existingImagesstate from entity data:
typescript1const [existingImages, setExistingImages] = useState<ExistingImageItem[]>( 2 () => { 3 return ( 4 entity?.images.map((image) => ({ 5 ...image, 6 isExisting: true, 7 toBeDeleted: false, 8 })) || [] 9 ); 10 } 11);
- Track images marked for deletion:
typescript1const imagesToDelete = useMemo(() => { 2 return existingImages.filter((image) => image.toBeDeleted); 3}, [existingImages]);
- Check if all images are being deleted:
typescript1const allImagesShouldBeDeleted = 2 imagesToDelete.length === existingImages.length;
Action Binding for Edit Mode
- Bind edit action with context for edit mode:
typescript1const updateEntityWithImagesToDelete = editEntityAd.bind(null, { 2 entityPublicId: entity?.publicId as string, 3 imagesToDelete, 4 allImagesShouldBeDeleted, 5});
- Conditionally use create or edit action:
typescript1const [formState, formAction, isPending] = useActionState( 2 isCreateMode ? publishEntityAd : updateEntityWithImagesToDelete, 3 undefined 4);
Form Default Values for Edit Mode
- Populate
defaultValueobject from entity data when in edit mode:
typescript1const [form, fields] = useForm({ 2 defaultValue: { 3 // String fields 4 fieldName: entity?.fieldName || "", 5 6 // Number fields - convert to string for form inputs 7 numericField: entity?.numericField?.toString() || "", 8 9 // Price fields - convert to string (PriceFormField will format with commas) 10 price: entity?.price?.toString() || "", 11 12 // Enum fields 13 enumField: entity?.enumField || "", 14 15 // Optional fields with defaults 16 optionalField: entity?.optionalField || "", 17 18 // Checkbox fields 19 acceptTerms: entity?.acceptTerms ? "on" : null, 20 21 // Arrays 22 images: [], 23 }, 24 // ... rest of form config 25});
- Always provide fallback values (empty strings, null, empty arrays)
- Convert numbers to strings for form inputs (they'll be converted back by schema)
- For checkboxes, use
"on"if entity value is true,nullif false/undefined
Image Handling in Edit Mode
- Update
DropFilesInputto account for existing images:
typescript1<DropFilesInput 2 // ... other props 3 existingFilesLength={ 4 existingImages.filter((image) => !image.toBeDeleted).length 5 } 6/>
- Update
ImagesPreviewerto handle existing images:
typescript1{(existingImages.length > 0 || selectedFiles.length > 0) && ( 2 <Box> 3 <ImagesPreviewer 4 existingImages={existingImages} 5 images={selectedFiles} 6 setImages={setSelectedFiles} 7 setExistingImages={setExistingImages} 8 maxImages={MAX_FILES} 9 /> 10 </Box> 11)}
- Show preview when there are existing images OR new selected files
Validation Schema Adjustments
- Adjust
minNumberOfImagesbased on deletion state:
typescript1return parseWithZod(updatedFormData, { 2 schema: createEntitySchema({ 3 minNumberOfImages: allImagesShouldBeDeleted ? 1 : 0, 4 }), 5});
- In edit mode, if all existing images are deleted, require at least 1 new image
- If some images remain, allow 0 new images (images are optional)
Submit Button Text
- Conditionally set button text based on mode:
typescript1<SubmitButton 2 pending={isPending} 3 disabled={acceptTerms.value !== "on"} 4 text={isCreateMode ? "Добавить объявление" : "Сохранить изменения"} 5/>
Page Component Updates
Create Page
- Pass
formMode={FormMode.Create}to form component:
typescript1<FormName formMode={FormMode.Create} />
Edit Page
- Fetch entity data using repository:
typescript1const entity = await entityRepository.getByPublicId(id); 2if (!entity) return notFound();
- Verify ownership:
typescript1const isOwner = await thisUserIsOwner(entity.user.id); 2if (!isOwner) return notFound();
- Pass entity and formMode to form:
typescript1<FormName entity={entity} formMode={FormMode.Edit} />
Edit Action Server Function Structure
- Edit action should accept context as first parameter:
typescript1export async function editEntityAd( 2 context: { 3 entityPublicId: string; 4 imagesToDelete: ExistingImageItem[]; 5 allImagesShouldBeDeleted: boolean; 6 }, 7 initialState: unknown, 8 formData: FormData 9) { 10 // ... implementation 11}
- Handle image deletion before processing new uploads
- Merge remaining existing images with newly uploaded images
- Use repository pattern to update entity
Error Handling
- Same error handling as create mode
- Use
ErrorModalcomponent - Reset form on modal close (same as create mode)
Loading States
- Same loading states as create mode
- Show
LoaderwhenisPendingis true - Disable all fields during submission
Best Practices
- Always check
isCreateModebefore accessingentityproperties - Use optional chaining (
entity?.property) when accessing entity data - Provide sensible defaults for all fields
- Ensure edit action properly handles partial updates
- Test both create and edit flows
- Verify image deletion and upload work correctly together
- Ensure validation works correctly in both modes
- Follow the pattern established in
ProfessionalServicePublishFormandCarPublishForm
Common Patterns
Conditional Rendering Based on Mode
typescript1{isCreateMode ? ( 2 <Text>Создание нового объявления</Text> 3) : ( 4 <Text>Редактирование объявления</Text> 5)}
Safe Entity Property Access
typescript1const defaultValue = entity?.property ?? fallbackValue;
Number Field Conversion
typescript1// In defaultValue 2numericField: entity?.numericField?.toString() || "", 3 4// Schema handles conversion back to number via z.coerce.number()