aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src')
-rw-r--r--FrontEnd/src/views/common/input/InputGroup.tsx197
1 files changed, 125 insertions, 72 deletions
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<Inputs extends Input[]> = {
- [Index in keyof Inputs]: boolean;
-};
+export type InputValueDict = Record<string, InputValue>;
+export type InputErrorDict = Record<string, Text>;
+export type InputDisabledDict = Record<string, boolean>;
+export type InputDirtyDict = Record<string, boolean>;
-type ExtendInputForComponent<I extends Input> = I & {};
+type MakeInputInfo<I extends Input> = Omit<I, "value" | "error" | "disabled">;
-type ExtendInputsForComponent<Inputs extends Input[]> = {
- [Index in keyof Inputs]: ExtendInputForComponent<Inputs[Index]>;
-};
+export type InputInfo = {
+ [I in Input as I["type"]]: MakeInputInfo<I>;
+}[Input["type"]];
-type InitialValueTransformer<I extends Input> = (
- 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<I>;
+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<InputState>;
+
+export type Initializer = {
+ scheme: InputScheme;
+ stateInit?: Partial<InputState>;
+};
-export interface InputGroupProps<Inputs extends Input[]> {
+export interface InputGroupProps {
color?: ThemeColor;
containerClassName?: string;
containerRef?: Ref<HTMLDivElement>;
- inputs: ExtendInputsForComponent<Inputs>;
- onChange: <Index extends number>(
- index: Index,
- value: InputValueMap[Inputs[Index]["type"]],
- ) => void;
+ inputs: Input[];
+ onChange: (index: number, value: Input["value"]) => void;
}
-export type ExtendInputForHook<I extends Input> = I & {
- initialValue?: InputInitialValueMap[I["type"]] | null;
-};
-
-export type ExtendInputsForHook<Inputs extends Input[]> = {
- [Index in keyof Inputs]: ExtendInputForHook<Inputs[Index]>;
-};
+function cleanObject<O extends Record<string, unknown>>(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<Inputs extends Input[]> = (
- values: { [Index in keyof Inputs]: InputValueMap[Inputs[Index]["type"]] },
- inputs: Inputs,
-) => InputErrors;
-
-export function useInputs<Inputs extends Input[]>(
- inputs: ExtendInputsForHook<Inputs>,
- options: {
- validator?: Validator<Inputs>;
- disabled?: boolean;
- },
-): {
- inputGroupProps: ExtendInputsForComponent<Inputs>;
- confirm: (values: Values<Inputs>) => 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<Values<Inputs>>(() =>
- inputs.map((input) =>
- defaultInitialValueTransformer[input.type](input, input.initialValue),
- ),
- );
- const [errors, setErrors] = useState<InputErrors>([]);
- const [dirties, setDirties] = useState<Dirties<Inputs>>();
+ 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<string, unknown>) => {
+ 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<Input>[] = [];
+ 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<Inputs extends Input[]>(
};
}
-export function InputGroup<Inputs extends Input[]>({
+export function InputGroup({
color,
inputs,
onChange,
containerRef,
containerClassName,
-}: InputGroupProps<ExtendInputsForComponent<Inputs>>) {
+}: InputGroupProps) {
const c = useC();
return (
@@ -202,9 +256,8 @@ export function InputGroup<Inputs extends Input[]>({
...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<Inputs extends Input[]>({
{label && <label className="cru-input-label">{c(label)}</label>}
<input
type={password ? "password" : "text"}
- value={value as string}
+ value={value}
onChange={(event) => {
const v = event.target.value;
changeValue(v);
@@ -233,7 +286,7 @@ export function InputGroup<Inputs extends Input[]>({
<div key={index} className={getContainerClassName()}>
<input
type="checkbox"
- checked={value as boolean}
+ checked={value}
onChange={(event) => {
const v = event.currentTarget.checked;
changeValue(v);
@@ -250,7 +303,7 @@ export function InputGroup<Inputs extends Input[]>({
<div key={index} className={getContainerClassName()}>
<label className="cru-input-label">{c(label)}</label>
<select
- value={value as string}
+ value={value}
onChange={(event) => {
const e = event.target.value;
changeValue(e);