1
\$\begingroup\$

This component is a multistep popup form designed to handle job vacancy creation. It uses MUI for the UI, Formik and Yup for form handling and validation. The form is divided into 4 steps: Job Details, Compensation, Skills & Requirements, and Process & Reapplication for a simple user experience. Each step includes specific fields validated with Yup. Users can navigate between steps and validation ensures required fields are completed before proceeding.

All of this works fine. But I'm looking for any suggestions mainly about typescript and architecture that I can improve or I'm doing wrong. I'd greatly appreciate that.

import { useState } from 'react';
import {
 Box,
 Button,
 Divider,
 Dialog,
 DialogContent,
 DialogTitle,
 IconButton,
 Stepper,
 Step,
 StepLabel,
} from '@mui/material';
import { Close as CloseIcon } from '@mui/icons-material';
import { Formik, Form, FormikErrors, FormikTouched } from 'formik'; // Importing Formik for form handling and validation
import * as Yup from 'yup'; // Importing Yup for schema based validation
import StepOne from './steps/StepOne'; // Import StepOne component for the first step of the form
import StepTwo from './steps/StepTwo'; // Import StepTwo component for the second step of the form
import StepThree from './steps/StepThree'; // Import StepThree component for the third step of the form
import StepFour from './steps/StepFour'; // Import StepFour component for the fourth step of the form
// Array of step titles that will be shown in the Stepper component for navigation
const steps = [
 'Job Details',
 'Compensation',
 'Skills & Requirements',
 'Process & Reapplication',
];
// Array of components corresponding to each form step
const stepsComponents = [<StepOne />, <StepTwo />, <StepThree />, <StepFour />];
const dialogActionStyles = {
 display: 'flex',
 justifyContent: 'center',
 gap: 2,
 py: 2,
 backgroundColor: '#f7f9fa',
 borderRadius: '6px',
};
// Define the prop types for the VacancyCreate component
interface VacancyCreateProps {
 open: boolean; // Controls whether the dialog is open
 setOpen: (newValue: boolean) => void; // Function to set the open state
}
// Define the shape of the form values object
interface FormValues {
 positionTitle: string;
 location: string | null;
 jobLevel: Record<string, unknown> | null;
 hiringManager: Record<string, unknown> | null;
 recruitFor: string | null;
 travelRequired: string;
 jobDescription: string;
 functionalArea: string;
 minPay: number;
 maxPay: number;
 internalResponsible: string | null;
 automata: string | null;
 reapplicationWaitingPeriod: number;
 reapplicationPeriodUnit: string;
 currency: string;
 responsibilities: string;
 skillsRequired: string;
 educationalRequirements: string;
}
// Initial values for the form fields
const initialValues: FormValues = {
 positionTitle: '', // Default empty string for position title
 location: '', // Default empty string for location
 jobLevel: null, // Default value of null for job level
 hiringManager: null, // Default value of null for hiring manager
 recruitFor: null, // Default value of null for recruit for field
 travelRequired: '', // Default empty string for travel required field
 jobDescription: '', // Default empty string for job description field
 functionalArea: '', // Default empty string for functional area field
 minPay: 0, // Default minimum pay is 0
 maxPay: 0, // Default maximum pay is 0
 internalResponsible: null, // Default value of null for internal responsible field
 automata: null, // Default value of null for automata field
 reapplicationWaitingPeriod: 3, // Default value of 3 for reapplication waiting period
 reapplicationPeriodUnit: 'months', // Default value of 'months' for the reapplication period unit
 currency: '', // Default empty string for currency
 responsibilities: '', // Default empty string for responsibilities
 skillsRequired: '', // Default empty string for skills required
 educationalRequirements: '', // Default empty string for educational requirements
};
// Grouping the fields into arrays for each step to control validation and rendering
const stepFieldGroups: Array<Array<keyof FormValues>> = [
 [
 'positionTitle',
 'location',
 'jobLevel',
 'hiringManager',
 'recruitFor',
 'travelRequired',
 'jobDescription',
 'functionalArea',
 ],
 ['minPay', 'maxPay', 'currency'],
 ['responsibilities', 'skillsRequired', 'educationalRequirements'],
 [
 'internalResponsible',
 'automata',
 'reapplicationWaitingPeriod',
 'reapplicationPeriodUnit',
 ],
];
// Defining the validation schema using Yup for each field in the form
const validationSchema = Yup.object().shape({
 positionTitle: Yup.string()
 .max(50)
 .required('Position Title is a required field'),
 location: Yup.string().nullable(),
 currency: Yup.string().nullable(),
 responsibilities: Yup.string().nullable(),
 skillsRequired: Yup.string().nullable(),
 educationalRequirements: Yup.string().nullable(),
 jobLevel: Yup.object().required('Job Level is a required field'),
 hiringManager: Yup.object().required('Hiring Manager is a required field'),
 recruitFor: Yup.mixed().nullable(),
 automata: Yup.mixed().nullable(),
 internalResponsible: Yup.mixed().nullable(),
 travelRequired: Yup.string().max(150).nullable(),
 jobDescription: Yup.string().nullable(),
 functionalArea: Yup.string().max(150).nullable(),
 reapplicationWaitingPeriod: Yup.number().required(
 'Reapplication Waiting Period is required'
 ),
 minPay: Yup.number()
 .min(0, 'Min pay must be greater than or equal to 0')
 .max(99999999.99, 'Min pay must be less than or equal to 99,999,999.99')
 .required('Min pay is required'),
 maxPay: Yup.number()
 .min(0, 'Max pay must be greater than or equal to 0')
 .max(99999999.99, 'Max pay must be less than or equal to 99,999,999.99')
 .required('Max pay is required'),
});
// Helper function to handle validation errors and set the appropriate form errors and touched fields
const handleValidationErrors = (
 err: Yup.ValidationError, // Yup validation error object
 currentStepFields: string[], // Fields for the current step
 setErrors: (errors: FormikErrors<FormValues>) => void, // Function to set form errors
 setTouched: (
 touched: FormikTouched<FormValues>,
 shouldValidate?: boolean
 ) => void // Function to set form touched status
) => {
 const errors = err.inner.reduce((acc: FormikErrors<FormValues>, error) => {
 acc[error.path as keyof FormValues] = error.message; // Assign the error message to the field
 return acc;
 }, {});
 // Mark the fields as touched to display validation errors
 const touchedFields = currentStepFields.reduce((acc, field) => {
 acc[field as keyof FormikTouched<FormValues>] = true; // Mark the field as touched
 return acc;
 }, {} as FormikTouched<FormValues>);
 setErrors(errors); // Set form errors
 setTouched(touchedFields, true); // Set form touched fields to show validation messages
};
// Main component to handle the vacancy creation form
export default function VacancyCreate({ open, setOpen }: VacancyCreateProps) {
 const [currentStep, setCurrentStep] = useState(0); // State to track the current form step
 // Function to close the dialog
 const handleClose = () => setOpen(false);
 // Function to move to the next step after validation
 const goToNextStep = async (
 values: FormValues, // Current form values
 setTouched: (
 touched: FormikTouched<FormValues>,
 shouldValidate?: boolean
 ) => void, // Function to mark fields as touched
 setErrors: (errors: FormikErrors<FormValues>) => void // Function to set validation errors
 ) => {
 const currentStepFields = stepFieldGroups[currentStep]; // Get the fields for the current step
 if (!currentStepFields) {
 return;
 }
 try {
 // Validate the fields for the current step using Yup.reach
 for (const field of currentStepFields) {
 const schema = Yup.reach(validationSchema, field) as Yup.Schema<any>; // Get the schema for the current field
 await schema.validate(values[field]); // Validate the current field's value
 }
 // If validation passes, proceed to the next step
 setCurrentStep((prevStep) => prevStep + 1);
 } catch (err) {
 if (err instanceof Yup.ValidationError) {
 // If there is a validation error, call the error handler
 handleValidationErrors(err, currentStepFields, setErrors, setTouched);
 }
 }
 };
 // Function to move to the previous step
 const goToPreviousStep = () => setCurrentStep((prevStep) => prevStep - 1);
 // Check if the current step is the last one
 const isLastStep = currentStep === stepsComponents.length - 1;
 // Check if the current step is the first one
 const isFirstStep = currentStep === 0;
 return (
 <Dialog fullWidth maxWidth='md' open={open} onClose={handleClose}>
 <DialogTitle sx={{ textAlign: 'center', py: 1.3 }}>
 New Vacancy
 </DialogTitle>
 <IconButton
 aria-label='close'
 onClick={handleClose}
 sx={{ position: 'absolute', right: 8, top: 8 }}
 >
 <CloseIcon />
 </IconButton>
 <Divider />
 <DialogContent sx={{ p: 0 }}>
 <Formik
 initialValues={initialValues}
 validationSchema={validationSchema}
 onSubmit={() => {}}
 >
 {({ values, setTouched, setErrors }) => (
 <Form noValidate>
 <Box sx={{ px: 6, py: 4 }}>
 <Stepper activeStep={currentStep} alternativeLabel>
 {steps.map((label) => (
 <Step key={label}>
 <StepLabel>{label}</StepLabel>
 </Step>
 ))}
 </Stepper>
 {/* Render the current step component */}
 {stepsComponents[currentStep]}
 </Box>
 <Divider />
 <Box sx={dialogActionStyles}>
 <Button size='small' variant='outlined' onClick={handleClose}>
 Cancel
 </Button>
 {!isFirstStep && (
 <Button
 size='small'
 variant='outlined'
 onClick={goToPreviousStep}
 >
 Back
 </Button>
 )}
 {isLastStep ? (
 <Button size='small' variant='contained' type='submit'>
 Submit
 </Button>
 ) : (
 <Button
 size='small'
 variant='contained'
 onClick={() => goToNextStep(values, setTouched, setErrors)}
 >
 Next
 </Button>
 )}
 </Box>
 </Form>
 )}
 </Formik>
 </DialogContent>
 </Dialog>
 );
}
Sᴀᴍ Onᴇᴌᴀ
29.5k16 gold badges45 silver badges201 bronze badges
asked Sep 12, 2024 at 0:57
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

A few suggestions

Infer types from schemas

Instead of writing both schemas and types, just write schema and infer type:

type User = InferType<typeof validationSchema>;

Group data by step

Group steps and stepsComponents, so all one step data would be in the same object. Otherwise you can easily forget update some variable when adding new steps.

answered Jan 7 at 15:44
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.