Sunday, June 15, 2025

TypeScript power ups autocomplete

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);
        },
    };
}