Skip to content

Instantly share code, notes, and snippets.

@owonwo
Last active June 15, 2023 11:32
Show Gist options
  • Select an option

  • Save owonwo/21b216412cfa0834e93e1cbccc565d7a to your computer and use it in GitHub Desktop.

Select an option

Save owonwo/21b216412cfa0834e93e1cbccc565d7a to your computer and use it in GitHub Desktop.
A snippet of my Form Builder component
import React, { useCallback } from "react";
import * as R from "ramda";
import styled from "@emotion/styled";
import { useFormContext } from "react-hook-form";
import {
DateTimePickerControl,
InputWrapper,
PasswordField,
RHFSelect,
RHFSelectDataSource,
Switch,
Textarea,
TextField,
} from "./FormFields";
import { FormField } from "../../libs/types/form";
import { makeTrace } from "../../libs/debugger";
import { IntlShape, useIntl } from "react-intl";
import { fieldTestId } from "../../libs/form.helpers";
import { ErrorType } from "../../libs/validators";
const [logger] = makeTrace("FormBuilder");
type FieldProp = {
field: FormField;
disabled?: boolean;
};
type FormBuilderProp = {
fields: FormField[];
fragment?: boolean;
};
export const getFieldErrorMessage = (
fieldName: string,
errors: Record<string, { message: string; type?: ErrorType }>,
intl: IntlShape
) => {
const error = R.path(fieldName.split("."), errors) as {
message: string;
type?: ErrorType;
};
if (!error) return null;
const message =
error.type && error.type !== ErrorType.Required
? intl.formatMessage({
id: error?.message,
defaultMessage: error?.message,
})
: error.message;
return message;
};
const removeFieldProps = R.omit([
"label",
"yupSchema",
"dataSource",
"options",
"explainer",
"extend",
"test",
]);
// TODO: Enable lazy evaluation for Builder Inputs
const RenderInput = ({ field: field_, disabled }: FieldProp) => {
const intl = useIntl();
const { register, formState, control } = useFormContext();
const translateText = useCallback(
(key?: string) => {
if (R.isEmpty(key)) return "";
return intl.formatMessage({
id: key,
defaultMessage: key,
});
},
[intl.locale]
);
const translateField = R.evolve({
label: translateText,
placeholder: translateText,
description: translateText,
});
const field = translateField(field_);
const message = getFieldErrorMessage(
field.name,
formState.errors as any,
intl
);
const muiProps = (field: FormField) => ({
name: field.name,
inputProps: {
disabled,
"data-testid": fieldTestId(field.name),
...removeFieldProps(field),
placeholder: "placeholder" in field ? field.placeholder : undefined,
},
});
if ("select" === field.type && field.dataSource?.key) {
return (
<InputWrapper label={field.label} errorMessage={message}>
<RHFSelectDataSource
{...muiProps(field)}
key={field.name}
placeholder={field.placeholder}
field={field}
/>
</InputWrapper>
);
}
if ("select" === field.type) {
return (
<InputWrapper label={field.label} errorMessage={message}>
<RHFSelect
{...muiProps(field)}
key={field.name}
options={field.options || []}
placeholder={field.placeholder}
name={field.name}
/>
</InputWrapper>
);
}
if (["email", "text"].includes(field.type))
return (
<InputWrapper label={field.label} errorMessage={message}>
<TextField {...muiProps(field)} label={null} />
</InputWrapper>
);
if ("password" === field.type) {
return (
<InputWrapper label={field.label} errorMessage={message}>
<PasswordField
label={null}
{...muiProps(field)}
{...register(field.name)}
/>
</InputWrapper>
);
}
if ("textarea" === field.type) {
return (
<InputWrapper label={field.label} errorMessage={message}>
<Textarea
{...muiProps(field)}
data-testid={"field-" + field.name}
{...register(field.name)}
/>
</InputWrapper>
);
}
if ("toggle" === field.type) {
return (
<InputWrapper errorMessage={message}>
<Switch name={field.name} label={field.label} control={control!} />
</InputWrapper>
);
}
if ("date" === field.type) {
return (
<InputWrapper label={field.label} errorMessage={message}>
<DateTimePickerControl name={field.name} control={control!} />
</InputWrapper>
);
}
if (field.type === "hidden")
return (
<input
data-testid={"field-" + field.name}
type="hidden"
{...register(field.name)}
/>
);
return <NoField field={field} reason={"Field isn't in registry"} />;
};
const NoField = ({ reason, field }: { reason: string; field: FormField }) => {
return (
<code className={"bg-orange-200 px-3 py-2 text-[12px] rounded-lg"}>
<span className={"font-semibold"}>Couldn&apos;t render input. </span>
<br />
{reason}
<br />
[FieldName={field.name}, FieldType={field.type}]<br />
</code>
);
};
export const Field = ({ field, disabled }: FieldProp) => {
const formCtx = useFormContext();
if (R.isNil(formCtx))
return <NoField field={field} reason={"Field is not in a FormContext."} />;
return (
<RenderInput
key={field.name}
field={field}
disabled={disabled ?? field?.disabled}
/>
);
};
export const FormBuilder: React.FC<FormBuilderProp> = ({
fields = [],
fragment,
}) => {
const { watch } = useFormContext();
const inputs = fields.map((field, idx) => {
const { hide } = field;
if (hide) {
const { name, value } = hide;
if (watch(name) === value) return null;
}
return <Field key={idx} field={field} />;
});
if (fragment) return <>{inputs}</>;
return (
<FormBuilderErrorBoundary>
<FormGrid>{inputs}</FormGrid>
</FormBuilderErrorBoundary>
);
};
const FormGrid = styled.div`
display: grid;
width: 100%;
grid-gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(280px, 450px));
`;
export class FormBuilderErrorBoundary extends React.Component<React.PropsWithChildren> {
state = {
error: false,
};
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
this.setState({ error: true });
console.log(error, errorInfo);
}
render() {
if (this.state.error) return <div>Something blow up</div>;
return this.props.children;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment