Skip to content

Commit

Permalink
fix: Portal frontend#7 (#49)
Browse files Browse the repository at this point in the history
* allow for nested properties

* fix validations

* forward min and max date

* show errors on submit

* copy
  • Loading branch information
SKairinos committed Jul 18, 2024
1 parent c8d3ae8 commit d4e595f
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 75 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
40 changes: 29 additions & 11 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 All @@ -21,8 +22,6 @@ export interface DatePickerFieldProps<
> {
name: string
required?: boolean
min?: string | Date
max?: string | Date
validateOptions?: ValidateOptions
}

Expand All @@ -32,18 +31,34 @@ const DatePickerField = <
>({
name,
required,
min,
max,
minDate,
maxDate,
validateOptions,
...otherDatePickerProps
}: DatePickerFieldProps<
TDate,
TEnableAccessibleFieldDOMStructure
>): JSX.Element => {
const dotPath = name.split(".")

function dateToString(date: Dayjs) {
return date.locale("en-gb").format("L")
}

let schema = YupDate()
if (required) schema = schema.required()
if (min) schema = schema.min(min)
if (max) schema = schema.max(max)
if (minDate) {
schema = schema.min(
minDate,
`this field must be after or equal to ${dateToString(minDate)}`,
)
}
if (maxDate) {
schema = schema.max(
maxDate,
`this field must be before or equal to ${dateToString(maxDate)}`,
)
}

const fieldConfig: FieldConfig = {
name,
Expand All @@ -54,7 +69,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 All @@ -73,6 +91,8 @@ const DatePickerField = <
<DatePicker
name={name}
value={value}
minDate={minDate}
maxDate={maxDate}
onChange={handleChange}
slotProps={{
textField: {
Expand All @@ -83,10 +103,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
33 changes: 27 additions & 6 deletions src/components/form/SubmitButton.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
import { Button, type ButtonProps } from "@mui/material"
import { Field, type FieldProps, type FormikProps } from "formik"
import { Field, type FieldProps } from "formik"
import type { FC } from "react"

export interface SubmitButtonProps
extends Omit<ButtonProps, "type" | "disabled"> {
disabled?: (form: FormikProps<any>) => boolean
}
extends Omit<ButtonProps, "type" | "onClick"> {}

const SubmitButton: FC<SubmitButtonProps> = ({
children = "Submit",
disabled = form => !(form.dirty && form.isValid),
...otherButtonProps
}) => {
function getTouched(
values: Record<string, any>,
touched?: Record<string, any>,
) {
touched = touched || {}
for (const key in values) {
const value = values[key]
touched[key] =
value instanceof Object && value.constructor === Object
? getTouched(value, touched)
: true
}

return touched
}

return (
<Field name="submit" type="submit">
{({ form }: FieldProps) => (
<Button type="submit" disabled={disabled(form)} {...otherButtonProps}>
<Button
type="button"
onClick={() => {
form.setTouched(getTouched(form.values), true).then(errors => {
if (!errors || !Object.keys(errors).length) form.submitForm()
})
}}
{...otherButtonProps}
>
{children}
</Button>
)}
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
Loading

0 comments on commit d4e595f

Please sign in to comment.