Skip to content

Commit

Permalink
allow for nested properties
Browse files Browse the repository at this point in the history
  • Loading branch information
SKairinos committed Jul 17, 2024
1 parent c8d3ae8 commit 2177dee
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 63 deletions.
63 changes: 35 additions & 28 deletions src/components/form/AutocompleteField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Field, type FieldConfig, type FieldProps } from "formik"
import { string as YupString, type ValidateOptions } from "yup"

import { schemaToFieldValidator } from "../../utils/form"
import { getNestedProperty } from "../../utils/general"

export interface AutocompleteFieldProps<
Multiple extends boolean | undefined = false,
Expand Down Expand Up @@ -60,6 +61,8 @@ const AutocompleteField = <
>): JSX.Element => {
const { name, required, ...otherTextFieldProps } = textFieldProps

const dotPath = name.split(".")

let schema = YupString().oneOf(options, "not a valid option")
if (required) schema = schema.required()

Expand All @@ -71,34 +74,38 @@ const AutocompleteField = <

return (
<Field {...fieldConfig}>
{({ form, meta }: FieldProps) => (
<Autocomplete
options={options}
defaultValue={
meta.initialValue === "" ? undefined : meta.initialValue
}
renderInput={({ id, ...otherParams }) => (
<TextField
id={name}
name={name}
required={required}
type="text"
value={form.values[name]}
error={form.touched[name] && Boolean(form.errors[name])}
helperText={
(form.touched[name] && form.errors[name]) as false | string
}
{...otherTextFieldProps}
{...otherParams}
/>
)}
onChange={(_, value) => {
form.setFieldValue(name, value ?? undefined, true)
}}
onBlur={form.handleBlur}
{...otherAutocompleteProps}
/>
)}
{({ form, meta }: FieldProps) => {
const value = getNestedProperty(form.values, dotPath)
const touched = getNestedProperty(form.touched, dotPath)
const error = getNestedProperty(form.errors, dotPath)

return (
<Autocomplete
options={options}
defaultValue={
meta.initialValue === "" ? undefined : meta.initialValue
}
renderInput={({ id, ...otherParams }) => (
<TextField
id={name}
name={name}
required={required}
type="text"
value={value}
error={touched && Boolean(error)}
helperText={(touched && error) as false | string}
{...otherTextFieldProps}
{...otherParams}
/>
)}
onChange={(_, value) => {
form.setFieldValue(name, value ?? undefined, true)
}}
onBlur={form.handleBlur}
{...otherAutocompleteProps}
/>
)
}}
</Field>
)
}
Expand Down
17 changes: 11 additions & 6 deletions src/components/form/CheckboxField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { type FC } from "react"
import { bool as YupBool, type ValidateOptions } from "yup"

import { schemaToFieldValidator } from "../../utils/form"
import { getNestedProperty } from "../../utils/general"

export interface CheckboxFieldProps
extends Omit<
Expand All @@ -31,6 +32,8 @@ const CheckboxField: FC<CheckboxFieldProps> = ({
validateOptions,
...otherCheckboxProps
}) => {
const dotPath = name.split(".")

let schema = YupBool()
if (required) schema = schema.oneOf([true], errorMessage)

Expand All @@ -43,28 +46,30 @@ const CheckboxField: FC<CheckboxFieldProps> = ({
return (
<Field {...fieldConfig}>
{({ form, meta }: FieldProps) => {
const error = form.touched[name] && Boolean(form.errors[name])
const touched = getNestedProperty(form.touched, dotPath)
const error = getNestedProperty(form.errors, dotPath)
const value = getNestedProperty(form.values, dotPath)

const hasError = touched && Boolean(error)

// https://mui.com/material-ui/react-checkbox/#formgroup
return (
<FormControl error={error} required={required}>
<FormControl error={hasError} required={required}>
<FormControlLabel
control={
<Checkbox
defaultChecked={meta.initialValue}
id={name}
name={name}
value={form.values[name]}
value={value}
onChange={form.handleChange}
onBlur={form.handleBlur}
{...otherCheckboxProps}
/>
}
{...formControlLabelProps}
/>
{error && (
<FormHelperText>{form.errors[name] as string}</FormHelperText>
)}
{hasError && <FormHelperText>{error as string}</FormHelperText>}
</FormControl>
)
}}
Expand Down
14 changes: 9 additions & 5 deletions src/components/form/DatePickerField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Field, type FieldConfig, type FieldProps } from "formik"
import { date as YupDate, type ValidateOptions } from "yup"

import { schemaToFieldValidator } from "../../utils/form"
import { getNestedProperty } from "../../utils/general"

export interface DatePickerFieldProps<
TDate extends PickerValidDate,
Expand Down Expand Up @@ -40,6 +41,8 @@ const DatePickerField = <
TDate,
TEnableAccessibleFieldDOMStructure
>): JSX.Element => {
const dotPath = name.split(".")

let schema = YupDate()
if (required) schema = schema.required()
if (min) schema = schema.min(min)
Expand All @@ -54,7 +57,10 @@ const DatePickerField = <
return (
<Field {...fieldConfig}>
{({ form }: FieldProps) => {
let value = form.values[name]
const error = getNestedProperty(form.errors, dotPath)
const touched = getNestedProperty(form.touched, dotPath)
let value = getNestedProperty(form.values, dotPath)

value = value ? dayjs(value) : null

function handleChange(value: Dayjs | null) {
Expand Down Expand Up @@ -83,10 +89,8 @@ const DatePickerField = <
},
onBlur: form.handleBlur,
required,
error: form.touched[name] && Boolean(form.errors[name]),
helperText: (form.touched[name] && form.errors[name]) as
| false
| string,
error: touched && Boolean(error),
helperText: (touched && error) as false | string,
},
}}
{...otherDatePickerProps}
Expand Down
23 changes: 15 additions & 8 deletions src/components/form/RepeatField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { string as YupString, type ValidateOptions } from "yup"

import { schemaToFieldValidator } from "../../utils/form"
import { getNestedProperty } from "../../utils/general"

export type RepeatFieldProps = Omit<
TextFieldProps,
Expand Down Expand Up @@ -45,9 +46,17 @@ const TextField: FC<
}) => {
const { form } = fieldProps

const dotPath = name.split(".")
const value = getNestedProperty(form.values, dotPath)

const repeatDotPath = repeatName.split(".")
const repeatValue = getNestedProperty(form.values, repeatDotPath)
const repeatTouched = getNestedProperty(form.touched, repeatDotPath)
const repeatError = getNestedProperty(form.errors, repeatDotPath)

useEffect(() => {
setValue(form.values[name])
})
setValue(value)
}, [setValue, value])

return (
<MuiTextField
Expand All @@ -57,13 +66,11 @@ const TextField: FC<
placeholder={placeholder ?? `Enter your ${name.replace("_", " ")} again`}
id={repeatName}
name={repeatName}
value={form.values[repeatName]}
value={repeatValue}
onChange={form.handleChange}
onBlur={form.handleBlur}
error={form.touched[repeatName] && Boolean(form.errors[repeatName])}
helperText={
(form.touched[repeatName] && form.errors[repeatName]) as false | string
}
error={repeatTouched && Boolean(repeatError)}
helperText={(repeatTouched && repeatError) as false | string}
{...otherTextFieldProps}
/>
)
Expand All @@ -78,7 +85,7 @@ const RepeatField: FC<RepeatFieldProps> = ({
}) => {
const [value, setValue] = useState("")

const repeatName = `repeat_${name}`
const repeatName = `${name}_repeat`

const fieldConfig: FieldConfig = {
name: repeatName,
Expand Down
39 changes: 23 additions & 16 deletions src/components/form/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { FC } from "react"
import { type StringSchema, type ValidateOptions } from "yup"

import { schemaToFieldValidator } from "../../utils/form"
import { getNestedProperty } from "../../utils/general"

export type TextFieldProps = Omit<
MuiTextFieldProps,
Expand All @@ -33,6 +34,8 @@ const TextField: FC<TextFieldProps> = ({
validateOptions,
...otherTextFieldProps
}) => {
const dotPath = name.split(".")

if (required) schema = schema.required()

const fieldConfig: FieldConfig = {
Expand All @@ -43,22 +46,26 @@ const TextField: FC<TextFieldProps> = ({

return (
<Field {...fieldConfig}>
{({ form }: FieldProps) => (
<MuiTextField
id={name}
name={name}
type={type}
required={required}
value={form.values[name]}
onChange={form.handleChange}
onBlur={form.handleBlur}
error={form.touched[name] && Boolean(form.errors[name])}
helperText={
(form.touched[name] && form.errors[name]) as false | string
}
{...otherTextFieldProps}
/>
)}
{({ form }: FieldProps) => {
const value = getNestedProperty(form.values, dotPath)
const error = getNestedProperty(form.errors, dotPath)
const touched = getNestedProperty(form.touched, dotPath)

return (
<MuiTextField
id={name}
name={name}
type={type}
required={required}
value={value}
onChange={form.handleChange}
onBlur={form.handleBlur}
error={touched && Boolean(error)}
helperText={(touched && error) as false | string}
{...otherTextFieldProps}
/>
)
}}
</Field>
)
}
Expand Down
21 changes: 21 additions & 0 deletions src/utils/general.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getNestedProperty } from "./general"

const PERSON = { father: { father: { name: "John" } } }

test("get a nested property with dot notation", () => {
const name = getNestedProperty(PERSON, "father.father.name")

expect(name).equal("John")
})

test("get a nested property with string array", () => {
const name = getNestedProperty(PERSON, ["father", "father", "name"])

expect(name).equal("John")
})

test("get a nested property that doesn't exist", () => {
const name = getNestedProperty(PERSON, "mother.mother.name")

expect(name).toBeUndefined()
})
19 changes: 19 additions & 0 deletions src/utils/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,3 +434,22 @@ export const UK_COUNTIES = [
] as const

export type UkCounties = (typeof UK_COUNTIES)[number]

export function getNestedProperty(
obj: Record<string, any>,
dotPath: string | string[],
): any {
if (typeof dotPath === "string") dotPath = dotPath.split(".")

let value: unknown = obj
for (let i = 0; i < dotPath.length; i++) {
value = (value as Record<string, any>)[dotPath[i]]
if (
i !== dotPath.length - 1 &&
(typeof value !== "object" || value === null)
)
return
}

return value
}

0 comments on commit 2177dee

Please sign in to comment.