Last active
June 15, 2023 11:32
-
-
Save owonwo/21b216412cfa0834e93e1cbccc565d7a to your computer and use it in GitHub Desktop.
A snippet of my Form Builder component
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'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