From dba8216b13a9473fd25674905e3048084794941e Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 27 Jul 2023 18:37:38 +0800 Subject: ... --- FrontEnd/src/views/common/input/InputGroup.tsx | 197 ++++++++++++++++--------- 1 file changed, 125 insertions(+), 72 deletions(-) (limited to 'FrontEnd/src/views') diff --git a/FrontEnd/src/views/common/input/InputGroup.tsx b/FrontEnd/src/views/common/input/InputGroup.tsx index 5040abae..7c33def7 100644 --- a/FrontEnd/src/views/common/input/InputGroup.tsx +++ b/FrontEnd/src/views/common/input/InputGroup.tsx @@ -23,7 +23,7 @@ * `useInputs` hook takes care of logic and generate props for `InputGroup`. */ -import { useState, Ref } from "react"; +import { useState, useRef, Ref } from "react"; import classNames from "classnames"; import { useC, Text, ThemeColor } from "../common"; @@ -63,92 +63,146 @@ export interface SelectInput extends InputBase { export type Input = TextInput | BoolInput | SelectInput; -interface InputInitialValueMap { - text: string; - bool: boolean; - select: string; -} +export type InputValue = Input["value"]; -type Dirties = { - [Index in keyof Inputs]: boolean; -}; +export type InputValueDict = Record; +export type InputErrorDict = Record; +export type InputDisabledDict = Record; +export type InputDirtyDict = Record; -type ExtendInputForComponent = I & {}; +type MakeInputInfo = Omit; -type ExtendInputsForComponent = { - [Index in keyof Inputs]: ExtendInputForComponent; -}; +export type InputInfo = { + [I in Input as I["type"]]: MakeInputInfo; +}[Input["type"]]; -type InitialValueTransformer = ( - input: I, - value: InputInitialValueMap[I["type"]] | null | undefined, -) => InputValueMap[I["type"]]; +export type Validator = ( + values: InputValueDict, + inputs: InputInfo[], +) => InputErrorDict; -type InitialValueTransformers = { - [I in Input as I["type"]]: InitialValueTransformer; +export type InputScheme = { + inputs: InputInfo[]; + validator?: Validator; }; -const defaultInitialValueTransformer: InitialValueTransformers = { - text: (input, value) => value ?? "", - bool: (input, value) => value ?? false, - select: (input, value) => value ?? input.options[0].value, +export type InputState = { + values: InputValueDict; + errors: InputErrorDict; + disabled: InputDisabledDict; + dirties: InputDirtyDict; }; -export type InputErrors = { - index: number; - message: Text; -}[]; +export type State = { + scheme: InputScheme; + state: InputState; +}; + +export type StateInitializer = Partial; + +export type Initializer = { + scheme: InputScheme; + stateInit?: Partial; +}; -export interface InputGroupProps { +export interface InputGroupProps { color?: ThemeColor; containerClassName?: string; containerRef?: Ref; - inputs: ExtendInputsForComponent; - onChange: ( - index: Index, - value: InputValueMap[Inputs[Index]["type"]], - ) => void; + inputs: Input[]; + onChange: (index: number, value: Input["value"]) => void; } -export type ExtendInputForHook = I & { - initialValue?: InputInitialValueMap[I["type"]] | null; -}; - -export type ExtendInputsForHook = { - [Index in keyof Inputs]: ExtendInputForHook; -}; +function cleanObject>(o: O): O { + const result = { ...o }; + for (const key of Object.keys(result)) { + if (result[key] == null) { + delete result[key]; + } + } + return result; +} -export type Validator = ( - values: { [Index in keyof Inputs]: InputValueMap[Inputs[Index]["type"]] }, - inputs: Inputs, -) => InputErrors; - -export function useInputs( - inputs: ExtendInputsForHook, - options: { - validator?: Validator; - disabled?: boolean; - }, -): { - inputGroupProps: ExtendInputsForComponent; - confirm: (values: Values) => void; +export function useInputs(options: { init?: () => Initializer }): { + inputGroupProps: InputGroupProps; } { - const { validator, disabled } = options; + function initializeValue( + input: InputInfo, + value?: InputValue | null, + ): InputValue { + if (input.type === "text") { + return value ?? ""; + } else if (input.type === "bool") { + return value ?? false; + } else if (input.type === "select") { + return value ?? input.options[0].value; + } + throw new Error("Unknown input type"); + } - const [values, setValues] = useState>(() => - inputs.map((input) => - defaultInitialValueTransformer[input.type](input, input.initialValue), - ), - ); - const [errors, setErrors] = useState([]); - const [dirties, setDirties] = useState>(); + function initialize(initializer: Initializer): State { + const { scheme, stateInit } = initializer; + const { inputs, validator } = scheme; + const keys = inputs.map((input) => input.key); + + if (process.env.NODE_ENV === "development") { + const checkKeys = (dict: Record) => { + for (const key of Object.keys(dict)) { + if (!keys.includes(key)) { + console.warn(""); + } + } + }; + + checkKeys(stateInit?.values ?? {}); + checkKeys(stateInit?.errors ?? {}); + checkKeys(stateInit?.disabled ?? {}); + checkKeys(stateInit?.dirties ?? {}); + } + + const values: InputValueDict = {}; + let errors: InputErrorDict = cleanObject( + initializer.stateInit?.errors ?? {}, + ); + const disabled: InputDisabledDict = cleanObject( + initializer.stateInit?.disabled ?? {}, + ); + const dirties: InputDirtyDict = cleanObject( + initializer.stateInit?.dirties ?? {}, + ); + + for (let i = 0; i < inputs.length; i++) { + const input = inputs[i]; + const { key } = input; + values[key] = initializeValue(input, stateInit?.values?.[key]); + + if (!(key in dirties)) { + dirties[key] = false; + } + } + + if (Object.keys(errors).length === 0 && validator != null) { + errors = validator(values, inputs); + } + + return { + scheme, + state: { + values, + errors, + disabled, + dirties, + }, + }; + } + + const { init } = options; - const componentInputs: ExtendInputForComponent[] = []; + const componentInputs: Input[] = []; 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; @@ -172,13 +226,13 @@ export function useInputs( }; } -export function InputGroup({ +export function InputGroup({ color, inputs, onChange, containerRef, containerClassName, -}: InputGroupProps>) { +}: InputGroupProps) { const c = useC(); return ( @@ -202,9 +256,8 @@ export function InputGroup({ ...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); + const changeValue = (value: InputValue) => { + onChange(index, value); }; if (type === "text") { @@ -217,7 +270,7 @@ export function InputGroup({ {label && } { const v = event.target.value; changeValue(v); @@ -233,7 +286,7 @@ export function InputGroup({
{ const v = event.currentTarget.checked; changeValue(v); @@ -250,7 +303,7 @@ export function InputGroup({