From a8d546d305e65f5126116d6bfe98b4f64906eb7d Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 26 Jul 2023 01:08:47 +0800 Subject: ... --- FrontEnd/src/views/common/input/InputGroup.tsx | 403 ++++++++++--------------- 1 file changed, 158 insertions(+), 245 deletions(-) (limited to 'FrontEnd/src/views/common') diff --git a/FrontEnd/src/views/common/input/InputGroup.tsx b/FrontEnd/src/views/common/input/InputGroup.tsx index 1b137fd8..5040abae 100644 --- a/FrontEnd/src/views/common/input/InputGroup.tsx +++ b/FrontEnd/src/views/common/input/InputGroup.tsx @@ -1,314 +1,259 @@ -import { - useState, - useEffect, - ReactNode, - ComponentPropsWithoutRef, - Ref, -} from "react"; +/** + * Some notes for InputGroup: + * This is one of the most complicated components in this project. + * Probably because the feature is complex and involved user inputs. + * + * I hope it contains following features: + * - Input features + * - Supports a wide range of input types. + * - Validator to validate user inputs. + * - Can set initial values. + * - Dirty, aka, has user touched this input. + * - Developer friendly + * - Easy to use APIs. + * - Type check as much as possible. + * - UI + * - Configurable appearance. + * - Can display helper and error messages. + * - Easy to extend, like new input types. + * + * So here is some design decisions: + * Inputs are identified by its _key_. + * `InputGroup` component takes care of only UI and no logic. + * `useInputs` hook takes care of logic and generate props for `InputGroup`. + */ + +import { useState, Ref } from "react"; import classNames from "classnames"; import { useC, Text, ThemeColor } from "../common"; import "./InputGroup.css"; -export interface TextInput { +export interface InputBase { + key: string; + label: Text; + helper?: Text; + disabled?: boolean; + error?: Text; +} + +export interface TextInput extends InputBase { type: "text"; - label?: Text; + value: string; password?: boolean; - textFieldProps?: Omit< - ComponentPropsWithoutRef<"input">, - "type" | "value" | "onChange" - >; - helperText?: Text; } -export interface BoolInput { +export interface BoolInput extends InputBase { type: "bool"; - label: Text; - helperText?: Text; + value: boolean; } export interface SelectInputOption { value: string; label: Text; - icon?: ReactNode; + icon?: string; } -export interface SelectInput { +export interface SelectInput extends InputBase { type: "select"; - label: Text; + value: string; options: SelectInputOption[]; } -export interface DateTimeInput { - type: "datetime"; - label?: Text; - helperText?: string; -} - -export type Input = TextInput | BoolInput | SelectInput | DateTimeInput; +export type Input = TextInput | BoolInput | SelectInput; -export type InputType = Input["type"]; - -export type InputTypeToInputMap = { - [I in Input as I["type"]]: I; -}; - -export interface InputTypeToValueMap { +interface InputInitialValueMap { 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; +type Dirties = { + [Index in keyof Inputs]: boolean; }; -export type MapInputListTo = { - [Index in keyof Tuple]: T; -}; +type ExtendInputForComponent = I & {}; -export interface InputTypeToInitialValueMap { - text: string | null | undefined; - bool: boolean | null | undefined; - select: string | null | undefined; - datetime: Date | string | null | undefined; -} +type ExtendInputsForComponent = { + [Index in keyof Inputs]: ExtendInputForComponent; +}; -export type MapInputToInitialValue = - InputTypeToInitialValueMap[I["type"]]; +type InitialValueTransformer = ( + input: I, + value: InputInitialValueMap[I["type"]] | null | undefined, +) => InputValueMap[I["type"]]; -export type InputValueTransformers = { - [I in Input as I["type"]]: ( - input: I, - value: MapInputToInitialValue, - ) => MapInputToValue; +type InitialValueTransformers = { + [I in Input as I["type"]]: InitialValueTransformer; }; -const initialValueTransformers: InputValueTransformers = { +const defaultInitialValueTransformer: InitialValueTransformers = { 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 interface InputGroupProps { + color?: ThemeColor; + containerClassName?: string; + containerRef?: Ref; + + inputs: ExtendInputsForComponent; + onChange: ( + index: Index, + value: InputValueMap[Inputs[Index]["type"]], + ) => void; +} + +export type ExtendInputForHook = I & { + initialValue?: InputInitialValueMap[I["type"]] | null; +}; -export function useInputs( +export type ExtendInputsForHook = { + [Index in keyof Inputs]: ExtendInputForHook; +}; + +export type Validator = ( + values: { [Index in keyof Inputs]: InputValueMap[Inputs[Index]["type"]] }, inputs: Inputs, - validator?: Validator, +) => InputErrors; + +export function useInputs( + inputs: ExtendInputsForHook, + options: { + validator?: Validator; + disabled?: boolean; + }, ): { - inputs: Inputs; - validator?: Validator; - dirties: Dirties | undefined; - setDirties: (dirties: Dirties) => void; - dirtyAll: () => void; + inputGroupProps: ExtendInputsForComponent; + confirm: (values: Values) => void; } { - const [dirties, setDirties] = useState>(); + const { validator, disabled } = options; - return { - inputs, - validator, - values, - dirties, - setDirties, - dirtyAll: () => { - if (dirties != null) { - setDirties(new Array(dirties.length).fill(true) as Dirties); - } - }, - }; -} + const [values, setValues] = useState>(() => + inputs.map((input) => + defaultInitialValueTransformer[input.type](input, input.initialValue), + ), + ); + const [errors, setErrors] = useState([]); + const [dirties, setDirties] = useState>(); -export interface InputGroupProps { - inputs: Inputs; - validator?: Validator; + const componentInputs: ExtendInputForComponent[] = []; + + for (let i = 0; i < inputs.length; i++) { + const input = { ...inputs[i] }; + delete input.initialValue; // No use. + const error = dirties[i] + ? errors.find((e) => e.index === i)?.message + : undefined; + const componentInput: ExtendInputForComponent = { + ...input, + value: values[i], + disabled, + error, + }; + componentInputs.push(componentInput); + } - 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; + const dirtyAll = () => { + if (dirties != null) { + setDirties(new Array(dirties.length).fill(true) as Dirties); + } + }; - color?: ThemeColor; - className?: string; - containerRef?: Ref; + return { + inputGroupProps: {}, + }; } -export default function InputGroup({ +export function InputGroup({ color, inputs, - validator, - values, - errors, - disabled, onChange, - dirties, - onDirty, containerRef, - className, -}: InputGroupProps) { + containerClassName, +}: 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") { + {inputs.map((item, index) => { + const { type, value, label, error, helper, disabled } = item; + + const getContainerClassName = ( + ...additionalClassNames: classNames.ArgumentArray + ) => + classNames( + `cru-input-container cru-input-${type}`, + error && "error", + ...additionalClassNames, + ); + + const changeValue = (value: InputValueMap[keyof InputValueMap]) => { + // `map` makes every type info lost, so we let ts do _not_ do type check here. + onChange(index, value as never); + }; + + if (type === "text") { + const { password } = item; return (
- {item.label && ( - - )} + {label && } { const v = event.target.value; - updateValue(index, v); + changeValue(v); }} disabled={disabled} /> - {error &&
{c(error)}
} - {item.helperText && ( -
- {c(item.helperText)} -
- )} + {error &&
{c(error)}
} + {helper &&
{c(helper)}
}
); - } else if (item.type === "bool") { + } else if (type === "bool") { return ( -
+
{ const v = event.currentTarget.checked; - updateValue(index, v); + changeValue(v); }} disabled={disabled} /> - - {error &&
{c(error)}
} - {item.helperText && ( -
- {c(item.helperText)} -
- )} + + {error &&
{c(error)}
} + {helper &&
{c(helper)}
}
); - } else if (item.type === "select") { + } else if (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)}
} -
- ); } })}
-- cgit v1.2.3