import { useState, useEffect, ReactNode, ComponentPropsWithoutRef, Ref, } from "react"; import classNames from "classnames"; import { useC, Text, ThemeColor } from "../common"; import "./InputGroup.css"; export interface TextInput { type: "text"; label?: Text; password?: boolean; textFieldProps?: Omit< ComponentPropsWithoutRef<"input">, "type" | "value" | "onChange" >; helperText?: Text; } export interface BoolInput { type: "bool"; label: Text; helperText?: Text; } export interface SelectInputOption { value: string; label: Text; icon?: ReactNode; } export interface SelectInput { type: "select"; label: Text; options: SelectInputOption[]; } export interface DateTimeInput { type: "datetime"; label?: Text; helperText?: string; } export type Input = TextInput | BoolInput | SelectInput | DateTimeInput; export type InputType = Input["type"]; export type InputTypeToInputMap = { [I in Input as I["type"]]: I; }; export interface InputTypeToValueMap { text: string; bool: boolean; select: string; datetime: Date; } export type InputValue = InputTypeToValueMap[keyof InputTypeToValueMap]; export type MapInputToValue = InputTypeToValueMap[I["type"]]; export type MapInputListToValueList = { [Index in keyof Tuple]: MapInputToValue; }; export type MapInputListTo = { [Index in keyof Tuple]: T; }; export interface InputTypeToInitialValueMap { text: string | null | undefined; bool: boolean | null | undefined; select: string | null | undefined; datetime: Date | string | null | undefined; } export type MapInputToInitialValue = InputTypeToInitialValueMap[I["type"]]; export type InputValueTransformers = { [I in Input as I["type"]]: ( input: I, value: MapInputToInitialValue, ) => MapInputToValue; }; const initialValueTransformers: InputValueTransformers = { text: (input, value) => value ?? "", bool: (input, value) => value ?? false, select: (input, value) => value ?? input.options[0].value, datetime: (input, value) => { if (value == null) return new Date(); if (typeof value === "string") { return new Date(value); } return value; }, }; // No use currently // // export type ValueValueTransformers = { // [I in Input as I["type"]]: (input: MapInputToValue) => MapInputToValue; // }; // // const finalValueMapperMap: ValueValueMapper = { // bool: (value) => value, // datetime: (value) => new Date(value).toISOString(), // select: (value) => value, // text: (value) => value, // }; export type InputErrors = { index: number; message: Text; }[]; export type InputList = Input[]; export type Validator = ( inputs: MapInputListToValueList, ) => InputErrors; export type Values = MapInputListToValueList; export type Dirties = MapInputListTo; export function useInputs( inputs: Inputs, validator?: Validator, ): { inputs: Inputs; validator?: Validator; dirties: Dirties | undefined; setDirties: (dirties: Dirties) => void; dirtyAll: () => void; } { const [dirties, setDirties] = useState>(); return { inputs, validator, values, dirties, setDirties, dirtyAll: () => { if (dirties != null) { setDirties(new Array(dirties.length).fill(true) as Dirties); } }, }; } export interface InputGroupProps { inputs: Inputs; validator?: Validator; values?: Values; onChange: ( values: Values, errors: InputErrors, trigger: number, // May be -1, which means I don't know who trigger this change. ) => void; errors: InputErrors; dirties: Dirties; onDirty: (dirties: Dirties) => void; disabled?: boolean; color?: ThemeColor; className?: string; containerRef?: Ref; } export default function InputGroup({ color, inputs, validator, values, errors, disabled, onChange, dirties, onDirty, containerRef, className, }: InputGroupProps) { const c = useC(); type Values = MapInputListToValueList; type Dirties = MapInputListTo; useEffect(() => { if (values == null) { const values = inputs.map((input) => { return initialValueTransformers[input.type](input as never); }) as Values; const errors = validator?.(values) ?? []; onChange(values, errors, -1); onDirty?.(inputs.map(() => false) as Dirties); } }, [values, inputs, validator, onChange, onDirty]); if (values == null) { return null; } const updateValue = (index: number, newValue: InputValue): void => { const oldValues = values; const newValues = oldValues.slice() as Values; newValues[index] = newValue; const error = validator?.(newValues) ?? []; onChange(newValues, error, index); if (dirties != null && onDirty != null && dirties[index] === false) { const newDirties = dirties.slice() as Dirties; newDirties[index] = true; onDirty(newDirties); } }; return (
{inputs.map((item: Input, index: number) => { const value = values[index]; const error = dirties && dirties[index] && errors && errors.find((e) => e.index === index)?.message; if (item.type === "text") { return (
{item.label && ( )} { const v = event.target.value; updateValue(index, v); }} disabled={disabled} /> {error &&
{c(error)}
} {item.helperText && (
{c(item.helperText)}
)}
); } else if (item.type === "bool") { return (
{ const v = event.currentTarget.checked; updateValue(index, v); }} disabled={disabled} /> {error &&
{c(error)}
} {item.helperText && (
{c(item.helperText)}
)}
); } else if (item.type === "select") { return (
); } else if (item.type === "datetime") { return (
{item.label && ( )} { const v = event.target.valueAsDate; if (v == null) { if (process.env.NODE_ENV === "development") { console.log( "Looks like user input date is null. We do nothing. But you might want to check why.", ); } return; } updateValue(index, v); }} disabled={disabled} /> {error &&
{c(error)}
}
); } })}
); }