I have a wizard form with the structure:
{
contactInfo:{
name: "string",
email: "string",
...
}
petInfo: {
petName: "string",
petBreed: "string,
...
}
vetInfo: {
vetName: "string",
vetPhone: "string",
...
}
}
At the top level of the component I render each section:
// Form Validation
import { yupResolver } from "@hookform/resolvers/yup";
import schema from "utils/FormValidation/wizardFormValidation";
import { defaultValues } from "utils/FormValidation/packFormValidation";
function getSteps(): string[] {
return ["Contact", "Emergency", "Pack", "Vet"];
}
function getStepContent(stepIndex: number): JSX.Element {
switch (stepIndex) {
case 0:
return <Contact />;
case 1:
return <Emergency />;
case 2:
return <Pack />;
case 3:
return <Vet />;
default:
return null;
}
}
function Welcome(): JSX.Element {
const dispatch = useAppDispatch();
const petParent = useAppSelector(selectPetParent);
const pack = useAppSelector(selectPack);
const wizard = useAppSelector((state) => state.forms.wizard);
const values = {
...petParent,
pack: pack.length !== 0 ? [...pack] : [...defaultValues],
} as Wizard;
const [activeStep, setActiveStep] = useState<number>(0);
const steps = getSteps();
const isLastStep: boolean = activeStep === steps.length - 1;
const handleNext = () => setActiveStep(activeStep + 1);
const handleBack = () => setActiveStep(activeStep - 1);
const methods = useForm({
mode: "all",
resolver: yupResolver(schema),
defaultValues: {
...wizard,
},
values: values,
});
const onSubmit = () => {
const formValues: Wizard = methods.getValues() as unknown as Wizard;
dispatch(submitWizardForm(formValues)).unwrap();
};
const handleDisabled = (): boolean => {
let section: any;
switch (activeStep) {
case 0:
section = "contactInfo";
break;
case 1:
section = "eContacts";
break;
case 2:
section = "pack";
break;
case 3:
section = "vetInfo";
break;
}
const fieldState = methods.getFieldState(section, methods.formState);
return activeStep !== 3 ? fieldState.invalid : !methods.formState.isValid;
};
useEffect(() => {
const subscription = methods.watch((value, { name, type }) => {
dispatch(updateWizardForm(value));
});
return () => subscription.unsubscribe();
}, [methods.watch]);
return (
<FormProvider {...methods}>
<div role="form" onSubmit={methods.handleSubmit(onSubmit)}>
<div>{getStepContent(activeStep)}</div>
{activeStep === 0 ? (
<div></div>
) : (
<button onClick={handleBack}>back</button>
)}
<button
disabled={handleDisabled()}
onClick={!isLastStep ? handleNext : onSubmit}
>
{isLastStep ? "submit" : "next"}
</button>
</div>
</FormProvider>
);
}
export default Welcome;
This issue I'm having is determining whether each section (contactInfo, petInfo, vetInfo) isValid
in order to properly handle whether to disable the next
button. At the moment handleDisabled
checks to see if the section is fieldState.invalid
but this doesn't always return false if the section of the form is blank. I tried checking the section fieldState.isTouched
and fieldState.isDirty
but those don't check for each field in the section just the section as a whole. Meaning if a user touches/dirties a single field in the section isTouched
/isDirty
return true and getFieldState.invalid
returns false which leads to users not knowing which fields they need to complete.
The methods on formState
are ideal but they only work on the form as a whole. I want to check if a specific section isValid
. Otherwise I would have to check each individual field using getFieldState
in each section.
I have looked over the documentation but the only solution I see is to break up each section into a separate form and then use formState.isValid
on each separate form section. Is there a way to keep the code as is and check isValid
by section?
Answers
To handle form validation for each section individually while keeping your current structure, you can modify your approach slightly. Instead of checking if the entire section is invalid, you can check if any of the fields in the section are invalid.
Here's how you can do it:
const handleDisabled = (): boolean => {
let section: any;
switch (activeStep) {
case 0:
section = "contactInfo";
break;
case 1:
section = "eContacts";
break;
case 2:
section = "pack";
break;
case 3:
section = "vetInfo";
break;
}
// Get field states for the current section
const sectionFields = methods.formState.dirtyFields;
const sectionErrors = methods.formState.errors;
// Check if any field in the section is invalid
const isSectionInvalid = Object.keys(sectionFields).some(
(fieldName) => fieldName.startsWith(section) && sectionErrors[fieldName]
);
// If it's not the last step, disable the next button if any field in the section is invalid
// If it's the last step, disable the next button if the entire form is invalid
return !isLastStep ? isSectionInvalid : !methods.formState.isValid;
};
In this approach:
- We get the dirty fields and errors from the form state.
- We check if any field in the current section has an error.
- If it's not the last step, we disable the next button if any field in the section is invalid.
- If it's the last step, we disable the next button if the entire form is invalid.
This way, users will know which fields they need to complete within each section, and the next button will be disabled accordingly.