265 lines
6.5 KiB
TypeScript
265 lines
6.5 KiB
TypeScript
import {
|
|
Checkbox,
|
|
CheckboxGroup,
|
|
FormControl,
|
|
FormErrorMessage,
|
|
Heading,
|
|
HStack,
|
|
Input,
|
|
NumberDecrementStepper,
|
|
NumberIncrementStepper,
|
|
NumberInput,
|
|
NumberInputField,
|
|
NumberInputStepper,
|
|
Radio,
|
|
RadioGroup,
|
|
Select,
|
|
Stack,
|
|
Textarea,
|
|
} from "@chakra-ui/react";
|
|
import { type Dispatch, type SetStateAction, useState } from "react";
|
|
|
|
interface component {
|
|
id: string;
|
|
max_length?: number;
|
|
options?: { default?: boolean; value: string }[];
|
|
required: boolean;
|
|
title: string;
|
|
type: string;
|
|
value?: number | string | string[];
|
|
}
|
|
|
|
export default function ({
|
|
components,
|
|
read_only = true,
|
|
}: {
|
|
components: { [k: number]: component[] };
|
|
read_only: boolean;
|
|
}) {
|
|
function isNumberElemInvalid(e: HTMLInputElement): boolean {
|
|
return !(
|
|
e.value ||
|
|
isNaN(e.valueAsNumber) ||
|
|
e.valueAsNumber <= Number.MAX_SAFE_INTEGER ||
|
|
e.valueAsNumber >= Number.MIN_SAFE_INTEGER
|
|
);
|
|
}
|
|
|
|
function updateState(
|
|
state: { [k: string]: string | string[] },
|
|
setState: Dispatch<SetStateAction<{}>>,
|
|
id: string,
|
|
value: string
|
|
) {
|
|
const newState = { ...state };
|
|
newState[id] = value;
|
|
|
|
setState(newState);
|
|
}
|
|
|
|
function renderCheckboxOptions(
|
|
c: component,
|
|
state: { [k: string]: string | string[] },
|
|
setState: Dispatch<SetStateAction<{}>>
|
|
) {
|
|
if (!c.options) throw new Error("Options for checkbox are undefined");
|
|
|
|
const boxes = [];
|
|
const checkedBoxes = [];
|
|
|
|
for (const option of c.options) {
|
|
if (
|
|
option.default ||
|
|
(read_only && Array.isArray(c.value) && c.value.includes(option.value))
|
|
)
|
|
checkedBoxes.push(option.value);
|
|
|
|
boxes.push(
|
|
<Checkbox
|
|
isReadOnly={read_only}
|
|
onChange={(e) => {
|
|
const newState = { ...state };
|
|
const groupValues = newState[c.id] ?? [];
|
|
|
|
if (!Array.isArray(groupValues))
|
|
throw new Error("Expected CheckboxGroup values to be an array");
|
|
|
|
e.target.checked
|
|
? groupValues.push(e.target.value)
|
|
: groupValues.splice(
|
|
groupValues.findIndex((v) => v === e.target.value),
|
|
1
|
|
);
|
|
|
|
newState[c.id] = groupValues;
|
|
setState(newState);
|
|
}}
|
|
value={option.value}
|
|
>
|
|
{option.value}
|
|
</Checkbox>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<CheckboxGroup defaultValue={checkedBoxes}>
|
|
<HStack spacing={5}>{boxes}</HStack>
|
|
</CheckboxGroup>
|
|
);
|
|
}
|
|
|
|
function renderRadioElements(
|
|
c: component,
|
|
state: { [k: string]: string | string[] },
|
|
setState: Dispatch<SetStateAction<{}>>
|
|
) {
|
|
if (!c.options) throw new Error("Options for radio buttons are undefined!");
|
|
const buttons = [];
|
|
|
|
for (const option of c.options) {
|
|
buttons.push(
|
|
<Radio checked={option.default} value={option.value}>
|
|
{option.value}
|
|
</Radio>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<RadioGroup
|
|
id={c.id}
|
|
onChange={(e) => {
|
|
const newState = { ...state };
|
|
newState[c.id] = e;
|
|
|
|
setState(newState);
|
|
}}
|
|
>
|
|
<Stack direction="row">{buttons}</Stack>
|
|
</RadioGroup>
|
|
);
|
|
}
|
|
|
|
function renderSelectElements(
|
|
c: component,
|
|
state: { [k: string]: string | string[] },
|
|
setState: Dispatch<SetStateAction<{}>>
|
|
) {
|
|
if (!c.options) throw new Error("Options for select are undefined!");
|
|
|
|
const selectOptions = [];
|
|
|
|
for (const option of c.options) {
|
|
selectOptions.push(<option value={option.value}>{option.value}</option>);
|
|
}
|
|
|
|
return (
|
|
<Select
|
|
onChange={(e) => {
|
|
const newState = { ...state };
|
|
newState[c.id] = e.target.value;
|
|
|
|
setState(newState);
|
|
}}
|
|
placeholder="Select option"
|
|
>
|
|
{selectOptions}
|
|
</Select>
|
|
);
|
|
}
|
|
|
|
function generateReactComponents(
|
|
components: component[],
|
|
state: { [k: string]: string | string[] },
|
|
setState: Dispatch<SetStateAction<{}>>
|
|
): JSX.Element[] {
|
|
const fragmentsList = [];
|
|
|
|
for (const component of components) {
|
|
fragmentsList.push(
|
|
<Heading size="md">{component.title}</Heading>,
|
|
<br />
|
|
);
|
|
|
|
switch (component.type) {
|
|
case "checkbox":
|
|
fragmentsList.push(renderCheckboxOptions(component, state, setState));
|
|
break;
|
|
|
|
case "input":
|
|
fragmentsList.push(
|
|
<FormControl
|
|
isInvalid={
|
|
!(document.getElementById(component.id) as HTMLInputElement)
|
|
.value.length
|
|
}
|
|
isReadOnly={read_only}
|
|
>
|
|
<Input
|
|
id={component.id}
|
|
maxLength={component.max_length}
|
|
onChange={(e) =>
|
|
updateState(state, setState, component.id, e.target.value)
|
|
}
|
|
placeholder="Your response"
|
|
value={component.value}
|
|
/>
|
|
<FormErrorMessage>Field is required</FormErrorMessage>
|
|
</FormControl>
|
|
);
|
|
break;
|
|
|
|
case "number":
|
|
fragmentsList.push(
|
|
<NumberInput
|
|
isInvalid={isNumberElemInvalid(
|
|
document.getElementById(component.id) as HTMLInputElement
|
|
)}
|
|
isReadOnly={read_only}
|
|
>
|
|
<NumberInputField
|
|
id={component.id}
|
|
onChange={(e) =>
|
|
updateState(state, setState, component.id, e.target.value)
|
|
}
|
|
value={component.value}
|
|
/>
|
|
<NumberInputStepper>
|
|
<NumberIncrementStepper />
|
|
<NumberDecrementStepper />
|
|
</NumberInputStepper>
|
|
</NumberInput>
|
|
);
|
|
break;
|
|
|
|
case "radio":
|
|
fragmentsList.push(renderRadioElements(component, state, setState));
|
|
break;
|
|
|
|
case "select":
|
|
fragmentsList.push(renderSelectElements(component, state, setState));
|
|
break;
|
|
}
|
|
|
|
fragmentsList.push(<br />, <br />, <br />);
|
|
}
|
|
|
|
return fragmentsList;
|
|
}
|
|
|
|
const pages = [];
|
|
const [responses, setResponses] = useState({});
|
|
|
|
for (const [page, componentList] of Object.entries(components)) {
|
|
pages.push(
|
|
<div
|
|
id={`form-page-${page}`}
|
|
style={{ display: page ? "none" : undefined }}
|
|
>
|
|
{generateReactComponents(componentList, responses, setResponses)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return <></>;
|
|
}
|