Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CareUI: Adds a higher-order Form component (not used anywhere) #4633

Merged
merged 9 commits into from
Feb 3, 2023
16 changes: 7 additions & 9 deletions src/Components/Common/DiagnosisSelectFormField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,15 @@ import { AutocompleteMutliSelect } from "../Form/FormFields/AutocompleteMultisel
import FormField from "../Form/FormFields/FormField";
import {
FormFieldBaseProps,
resolveFormFieldChangeEventHandler,
useFormFieldPropsResolver,
} from "../Form/FormFields/Utils";

type Props =
// | ({ multiple?: false | undefined } & FormFieldBaseProps<ICD11DiagnosisModel>) // uncomment when single select form field is required and implemented.
{ multiple: true } & FormFieldBaseProps<ICD11DiagnosisModel[]>;

export function DiagnosisSelectFormField(props: Props) {
const { name } = props;
const handleChange = resolveFormFieldChangeEventHandler(props);

const field = useFormFieldPropsResolver(props);
const { fetchOptions, isLoading, options } =
useAsyncOptions<ICD11DiagnosisModel>("id");

Expand All @@ -28,17 +26,17 @@ export function DiagnosisSelectFormField(props: Props) {
}

return (
<FormField props={props}>
<FormField field={field}>
<AutocompleteMutliSelect
id={props.id}
disabled={props.disabled}
value={props.value || []}
id={field.id}
disabled={field.disabled}
value={field.value || []}
onChange={field.handleChange}
options={options(props.value)}
optionLabel={(option) => option.label}
optionValue={(option) => option}
onQuery={(query) => fetchOptions(listICD11Diagnosis({ query }, ""))}
isLoading={isLoading}
onChange={(value) => handleChange({ name, value })}
/>
</FormField>
);
Expand Down
17 changes: 8 additions & 9 deletions src/Components/Common/SymptomsSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { SYMPTOM_CHOICES } from "../../Common/constants";
import FormField from "../Form/FormFields/FormField";
import {
FormFieldBaseProps,
resolveFormFieldChangeEventHandler,
useFormFieldPropsResolver,
} from "../Form/FormFields/Utils";
import MultiSelectMenuV2 from "../Form/MultiSelectMenuV2";

Expand All @@ -18,12 +18,11 @@ const ASYMPTOMATIC_ID = 1;
* - For other scenarios, this simply works like a `MultiSelect`.
*/
export const SymptomsSelect = (props: FormFieldBaseProps<number[]>) => {
const { name } = props;
const handleChange = resolveFormFieldChangeEventHandler(props);
const field = useFormFieldPropsResolver(props);

const updateSelection = (value: number[]) => {
// Skip the complexities if no initial value was present
if (!props.value?.length) return handleChange({ name, value });
if (!props.value?.length) return field.handleChange(value);

const initialValue = props.value || [];

Expand All @@ -33,16 +32,16 @@ export const SymptomsSelect = (props: FormFieldBaseProps<number[]>) => {
if (asymptomaticIndex > -1) {
// unselect asym.
value.splice(asymptomaticIndex, 1);
return handleChange({ name, value });
return field.handleChange(value);
}
}

if (!initialValue.includes(ASYMPTOMATIC_ID) && value.includes(1)) {
// If new selections have asym., unselect everything else
return handleChange({ name, value: [ASYMPTOMATIC_ID] });
return field.handleChange([ASYMPTOMATIC_ID]);
}

handleChange({ name, value });
field.handleChange(value);
};

const getDescription = ({ id }: { id: number }) => {
Expand All @@ -69,9 +68,9 @@ export const SymptomsSelect = (props: FormFieldBaseProps<number[]>) => {
};

return (
<FormField props={props}>
<FormField field={field}>
<MultiSelectMenuV2
id={props.id}
id={field.id}
options={SYMPTOM_CHOICES}
disabled={props.disabled}
placeholder="Select symptoms"
Expand Down
12 changes: 7 additions & 5 deletions src/Components/Form/FieldValidators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ export type FieldValidator<T> = (value: T) => FieldError;
* <EmailField
* ...
* validate={MultiValidator([
* RequiredFieldValidator,
* EmailValidator,
* RequiredFieldValidator(),
* EmailValidator(),
* ])}
* ...
* />
Expand All @@ -29,7 +29,9 @@ export const MultiValidator = <T>(
return validator;
};

export const RequiredFieldValidator = <T>(value: T): FieldError => {
if (value === undefined || (typeof value === "string" && value === ""))
return "Field is required";
export const RequiredFieldValidator = (message = "Field is required") => {
return <T>(value: T): FieldError => {
if (!value) return message;
if (Array.isArray(value) && value.length === 0) return message;
};
};
123 changes: 123 additions & 0 deletions src/Components/Form/Form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { isEmpty, omitBy } from "lodash";
import { useEffect, useReducer, useState } from "react";
import { classNames } from "../../Utils/utils";
import { Cancel, Submit } from "../Common/components/ButtonV2";
import { FieldValidator } from "./FieldValidators";
import { FormContext, FormContextValue } from "./FormContext";
import { FieldChangeEvent } from "./FormFields/Utils";
import { FormDetails, FormErrors, formReducer, FormReducer } from "./Utils";

type Props<T extends FormDetails> = {
context: FormContext<T>;
className?: string;
defaults: T;
asyncGetDefaults?: (() => Promise<T>) | false;
onlyChild?: boolean;
validate?: (form: T) => FormErrors<T>;
onSubmit: (form: T) => Promise<FormErrors<T> | void>;
onCancel?: () => void;
noPadding?: true;
disabled?: boolean;
submitLabel?: string;
cancelLabel?: string;
children: (props: FormContextValue<T>) => React.ReactNode;
};

const Form = <T extends FormDetails>({
asyncGetDefaults,
validate,
...props
}: Props<T>) => {
const initial = { form: props.defaults, errors: {} };
const [isLoading, setIsLoading] = useState(!!asyncGetDefaults);
const [state, dispatch] = useReducer<FormReducer<T>>(formReducer, initial);

useEffect(() => {
if (!asyncGetDefaults) return;

asyncGetDefaults().then((form) => {
dispatch({ type: "set_form", form });
setIsLoading(false);
});
}, [asyncGetDefaults]);

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();

if (validate) {
const errors = omitBy(validate(state.form), isEmpty) as FormErrors<T>;

if (Object.keys(errors).length) {
dispatch({ type: "set_errors", errors });
return;
}
}

const errors = await props.onSubmit(state.form);
if (errors) {
dispatch({
type: "set_errors",
errors: { ...state.errors, ...errors },
});
}
};

const { Provider, Consumer } = props.context;
const disabled = isLoading || props.disabled;

return (
<form
onSubmit={handleSubmit}
className={classNames(
"bg-white rounded w-full max-w-3xl mx-auto",
!props.noPadding && "px-8 md:px-16 py-5 md:py-11",
props.className
)}
noValidate
>
<Provider
value={(name: keyof T, validate?: FieldValidator<T[keyof T]>) => {
return {
name,
id: name,
onChange: ({ name, value }: FieldChangeEvent<T[keyof T]>) =>
dispatch({
type: "set_field",
name,
value,
error: validate && validate(value),
}),
value: state.form[name],
error: state.errors[name],
disabled,
};
}}
>
{props.onlyChild ? (
<Consumer>{props.children}</Consumer>
) : (
<>
<div className="my-6 flex flex-col gap-4">
<Consumer>{props.children}</Consumer>
</div>
<div className="flex justify-end gap-2">
<Cancel
className="w-full md:w-auto"
onClick={props.onCancel}
label={props.cancelLabel ?? "Cancel"}
/>
<Submit
type="submit"
className="w-full md:w-auto"
disabled={disabled}
label={props.submitLabel ?? "Submit"}
/>
</div>
</>
)}
</Provider>
</form>
);
};

export default Form;
19 changes: 19 additions & 0 deletions src/Components/Form/FormContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Context, createContext } from "react";
import { FieldError, FieldValidator } from "./FieldValidators";
import { FormDetails } from "./Utils";

export type FormContextValue<T extends FormDetails> = (
name: keyof T,
validate?: FieldValidator<T[keyof T]>
) => {
id: keyof T;
name: keyof T;
onChange: any;
value: any;
error: FieldError | undefined;
};

export type FormContext<T extends FormDetails> = Context<FormContextValue<T>>;

export const createFormContext = <T extends FormDetails>() =>
createContext<FormContextValue<T>>(undefined as any);
23 changes: 9 additions & 14 deletions src/Components/Form/FormFields/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import { Combobox } from "@headlessui/react";
import { DropdownTransition } from "../../Common/components/HelperComponents";
import CareIcon from "../../../CAREUI/icons/CareIcon";
import { dropdownOptionClassNames } from "../MultiSelectMenuV2";
import {
FormFieldBaseProps,
resolveFormFieldChangeEventHandler,
} from "./Utils";
import { FormFieldBaseProps, useFormFieldPropsResolver } from "./Utils";
import FormField from "./FormField";

type OptionCallback<T, R> = (option: T) => R;
Expand All @@ -24,24 +21,22 @@ type AutocompleteFormFieldProps<T, V> = FormFieldBaseProps<V> & {
const AutocompleteFormField = <T, V>(
props: AutocompleteFormFieldProps<T, V>
) => {
const { name } = props;
const handleChange = resolveFormFieldChangeEventHandler(props);

const field = useFormFieldPropsResolver(props);
return (
<FormField props={props}>
<FormField field={field}>
<Autocomplete
id={props.id}
id={field.id}
disabled={field.disabled}
required={field.required}
className={field.className}
value={field.value}
onChange={(value: any) => field.handleChange(value)}
options={props.options}
disabled={props.disabled}
value={props.value}
placeholder={props.placeholder}
optionLabel={props.optionLabel}
optionIcon={props.optionIcon}
optionValue={props.optionValue}
className={props.className}
required={props.required}
onQuery={props.onQuery}
onChange={(value: any) => handleChange({ name, value })}
/>
</FormField>
);
Expand Down
16 changes: 6 additions & 10 deletions src/Components/Form/FormFields/AutocompleteMultiselect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import React, { useEffect, useState } from "react";
import { Combobox } from "@headlessui/react";
import { DropdownTransition } from "../../Common/components/HelperComponents";
import CareIcon from "../../../CAREUI/icons/CareIcon";
import {
FormFieldBaseProps,
resolveFormFieldChangeEventHandler,
} from "./Utils";
import { FormFieldBaseProps, useFormFieldPropsResolver } from "./Utils";
import FormField from "./FormField";
import {
dropdownOptionClassNames,
Expand All @@ -26,15 +23,14 @@ type AutocompleteMultiSelectFormFieldProps<T, V> = FormFieldBaseProps<V[]> & {
const AutocompleteMultiSelectFormField = <T, V>(
props: AutocompleteMultiSelectFormFieldProps<T, V>
) => {
const { name } = props;
const handleChange = resolveFormFieldChangeEventHandler(props);

const field = useFormFieldPropsResolver(props);
return (
<FormField props={props}>
<FormField field={field}>
<AutocompleteMutliSelect
{...props}
value={props.value || []}
onChange={(value) => handleChange({ name, value })}
{...field}
value={field.value || []}
onChange={field.handleChange}
/>
</FormField>
);
Expand Down
27 changes: 10 additions & 17 deletions src/Components/Form/FormFields/DateFormField.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { classNames } from "../../../Utils/utils";
import DateInputV2, { DatePickerPosition } from "../../Common/DateInputV2";
import FormField from "./FormField";
import {
FormFieldBaseProps,
resolveFormFieldChangeEventHandler,
resolveFormFieldError,
} from "./Utils";
import { FormFieldBaseProps, useFormFieldPropsResolver } from "./Utils";

type Props = FormFieldBaseProps<Date> & {
placeholder?: string;
Expand All @@ -30,22 +26,19 @@ type Props = FormFieldBaseProps<Date> & {
* />
* ```
*/
const DateFormField = ({ position = "RIGHT", ...props }: Props) => {
const handleChange = resolveFormFieldChangeEventHandler(props);
const error = resolveFormFieldError(props);
const name = props.name;

const DateFormField = (props: Props) => {
const field = useFormFieldPropsResolver(props as any);
return (
<FormField props={props}>
<FormField field={field}>
<DateInputV2
className={classNames(error && "border-red-500")}
id={props.id}
value={props.value}
onChange={(value) => handleChange({ name, value })}
className={classNames(field.error && "border-red-500")}
id={field.id}
value={field.value}
onChange={field.handleChange}
disabled={field.disabled}
max={props.max || (props.disableFuture ? new Date() : undefined)}
min={props.min || (props.disablePast ? yesterday() : undefined)}
position={position}
disabled={props.disabled}
position={props.position || "RIGHT"}
placeholder={props.placeholder}
/>
</FormField>
Expand Down
Loading