DonationCandidateForm.tsx:
import {
Button,
FormControl,
Grid,
InputLabel,
MenuItem,
Select,
TextField,
} from "@mui/material";
import { useState, type FormEvent } from "react";
import { setupInputProps, type ErrorObject } from "../utils";
const initialFieldValues = {
fullname: "",
mobile: "",
email: "",
age: 0,
bloodGroup: "",
address: "",
favoriteColors: [],
};
type FormType = typeof initialFieldValues;
// out-of-band validations
type OtherStates = {
areZombiesInLab: boolean;
day?: number;
};
// const errors: Partial<Record<FormType, string>> = {};
// const initialErrors: ErrorObject<FormType> = {};
export default function DonationCandidateForm() {
const [values, setValues] = useState(initialFieldValues);
const [errors, setErrors] = useState<ErrorObject<FormType & OtherStates>>(
{}
);
// const handleInputChange = setupInputChange(values, setValues);
// const handleErrorChange = setupErrorChange(errors);
const { handleInputProps, checkValidators } = setupInputProps(
values,
setValues,
errors,
setErrors
);
function handleSubmit(event: FormEvent<HTMLFormElement>): void {
event.preventDefault();
const otherStates: OtherStates = { areZombiesInLab: true };
console.log(values);
const isValid = checkValidators({
fullname: !values.fullname && "Must have full name",
age: !(values.age >= 18) && "Must be an adult",
favoriteColors:
!(values.favoriteColors.length > 0) &&
"Must have favorite colors",
areZombiesInLab:
otherStates.areZombiesInLab &&
"Zombies around, data won't be saved, you must escape now for your own safety",
});
if (!isValid) {
return;
}
// if (hasErrors(errors)) {
// return;
// }
}
return (
<>
{JSON.stringify(values)}
<form autoComplete="off" noValidate onSubmit={handleSubmit}>
<Grid container marginBottom={1}>
<Grid size={{ xs: 6 }}>
<TextField
label="Full name"
// error={true}
// helperText="Something"
required={true}
{...handleInputProps("fullname")}
/>
<TextField
label="Mobile"
{...handleInputProps("mobile")}
/>
<TextField
label="Email"
{...handleInputProps("email")}
/>
</Grid>
<Grid size={{ xs: 6 }}>
<TextField label="Age" {...handleInputProps("age")} />
<FormControl>
<InputLabel>Blood Group</InputLabel>
<Select {...handleInputProps("bloodGroup")}>
<MenuItem value="">Select Blood Group</MenuItem>
<MenuItem value="A*">A *ve+</MenuItem>
<MenuItem value="A-">A -ve</MenuItem>
<MenuItem value="B+">B +ve</MenuItem>
<MenuItem value="B-">B -ve</MenuItem>
<MenuItem value="AB+">AB +ve</MenuItem>
<MenuItem value="AB-">AB -ve</MenuItem>
<MenuItem value="O+">O +ve</MenuItem>
<MenuItem value="O-">O -ve</MenuItem>
</Select>
</FormControl>
<TextField
label="address"
{...handleInputProps("address")}
/>
<div>
<Button
color="primary"
type="submit"
variant="contained"
>
Submit
</Button>
<Button
type="submit"
color="inherit"
variant="contained"
>
Reset
</Button>
</div>
</Grid>
</Grid>
{errors.favoriteColors && <div>{errors.favoriteColors}</div>}
{errors.areZombiesInLab && <div>{errors.areZombiesInLab}</div>}
</form>
</>
);
}
utils.ts:
type UnifiedChangeEvent =
| React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
| React.ChangeEvent<{ name?: string; value: unknown }>
| (Event & { target: { name: string; value: unknown } });
export type ErrorObject<T> = Partial<Record<keyof T, string | boolean>>;
function handleInputChange<T>(
values: T,
setValues: (o: T) => void,
fieldName: keyof T
) {
return {
onChange(event: UnifiedChangeEvent) {
const { name, value } = event.target;
setValues({ ...values, [name!]: value });
},
name: fieldName,
value: values[fieldName],
};
}
export function setupInputChange<T>(values: T, setValues: (o: T) => void) {
return (fieldName: keyof T) =>
handleInputChange(values, setValues, fieldName);
}
function handleErrorChange<T>(errors: T, fieldName: keyof T) {
return errors[fieldName] && { error: true, helperText: errors[fieldName] };
}
export function setupErrorChange<T>(errors: T) {
return (fieldName: keyof T) => handleErrorChange(errors, fieldName);
}
export function isValid<T>(errors: ErrorObject<T>): boolean {
for (const v of Object.values(errors)) {
if (v) {
return false;
}
}
return true;
}
export function hasErrors<T>(errors: ErrorObject<T>) {
return !isValid(errors);
}
export function setupInputProps<T, U>(
values: T,
setValues: (o: T) => void,
errors: ErrorObject<U>,
setErrors: (o: ErrorObject<U>) => void
) {
return {
handleInputProps: (fieldName: keyof T & keyof U) => ({
...handleInputChange(values, setValues, fieldName),
...handleErrorChange(errors, fieldName),
}),
checkValidators: (errors: ErrorObject<U>) => {
setErrors(errors);
return isValid(errors);
},
};
}