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

Saturday, December 7, 2024

Wednesday, March 15, 2023

ESLint wish

const y = new Set([10,20,30]);
const z = new Set([7,8,9]);

[1,2,3,4].forEach(y.add, z); // oops you added to z instead

console.log([...y]);
console.log([...z]);

// can't fault those who prefer this. I mean this line, not javascript's this :D
[1,2,3,4].forEach(Set.prototype.add, y); 

console.log([...y]);
console.log('---');
console.log([...z]);
If you are used to other language that automatically pass the this to the callback, you might want to be more explicit in JavaScript on passing which function you want to be called instead

Wish ESLint has a warning for passing callback like in line 4
[10, 20, 30]
[7, 8, 9, 1, 2, 3, 4]
---
[10, 20, 30, 1, 2, 3, 4]
[7, 8, 9, 1, 2, 3, 4]

Friday, January 27, 2023

Browsers translation mechanism

Safari can translate across tag boundaries. The phrase 先开发 (先 = first, 开发 = develop. in English is develop first), is split and the subject is placed between the the words that were split, i.e., develop something something here first



Chrome can only translate tag by tag. Each one of its translation is contained in the same tag the source language are contained in originally


Safari's advantage is that it can re-arrange the translation across tags, making the translation sound more natural than Chrome. Chinese words separator app highlighter granularity is by phrase, it stops at commas and periods, each phrase/sentence is contained in a tag

Chrome's advantage, though sounding unnatural English, is that you can get a feel of the grammar of Chinese language and how its sentence is structured


Chinese words separator for Safari

Chinese words separator for Chrome

Friday, July 1, 2022

Preventing accidental alert and console.log on production

Use this regular expression to find stray console.log and alert:
^[^\/]\s+console\.group|^console\.group
^[^\/]\s+console\.log|^console\.log
^[^\/]\s+alert|^alert

Saturday, June 11, 2022

Multiple nots is knotty to read

Simplifying multiple nots:
const isVariant = e.match(VARIANT_TESTER);
const matchingFirstHanzi = isVariant?.[2];
const matchingSecondHanzi = isVariant?.[4];

// knotty read:
// const needToShow
//     = !isVariant || (matchingFirstHanzi !== hanzi && matchingSecondHanzi !== hanzi);

// De-Morgan'd, easy to read:
// const needToHide = isVariant && (matchingFirstHanzi === hanzi || matchingSecondHanzi === hanzi);

const needToHide = isVariant && [matchingFirstHanzi, matchingSecondHanzi].includes(hanzi);
const needToShow = !needToHide;