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>
);
}
1 Answer 1
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.
Explore related questions
See similar questions with these tags.