// FormPlaygroundProvider.tsx
import { zodResolver } from '@hookform/resolvers/zod'
import { FormProvider, useForm } from 'react-hook-form'
import {
formPlaygroundSchema,
type OutputFormValues,
type InputFormValues,
} from './formPlaygroundSchema'
type Props = {
children: React.ReactNode
}
/** React Hook Form provider connected to zod schema */
export const FormPlaygroundProvider = (props: Props): React.JSX.Element => {
const schema = formPlaygroundSchema.check((ctx) => {
// Inside check() we may validate schema based on external data
const apiResponse = {
text: 'hello',
}
if (!ctx.value.textarea.toLowerCase().includes(apiResponse.text)) {
ctx.issues.push({
code: 'custom',
message: `Should contain "hello" text from api response`,
input: ctx.value, // original failed value at schema.safeParse() output for debugging
path: ['textarea'], // ["foo","bar","baz"] → ctx.value.foo.bar.baz
continue: true, // without "true" check stops
})
}
})
const formMethods = useForm<InputFormValues, unknown, OutputFormValues>({
resolver: zodResolver(schema),
defaultValues: {
input: 'input',
autocomplete: 'autocomplete',
autocompleteWithFreeText: 'autocompleteWithFreeText',
textarea: 'textarea',
date: new Date(),
checkbox: false,
select: '',
toggle: true,
},
})
return <FormProvider {...formMethods}>{props.children}</FormProvider>
}
// formPlaygroundSchema.ts
import { z, type ZodType } from 'zod/v4'
const zStringNotEmpty = z.string().trim().min(1, { error: 'Error message' })
const zDate = z.date({ error: 'Error message' })
const zTrue: ZodType<true, boolean> = z.preprocess(
(val) => val,
z.literal(true, { error: 'Should be checked' }),
)
export const formPlaygroundSchema = z
.object({
input: zStringNotEmpty,
autocomplete: zStringNotEmpty,
autocompleteWithFreeText: zStringNotEmpty,
textarea: zStringNotEmpty,
date: zDate,
checkbox: zTrue,
select: zStringNotEmpty,
toggle: z.boolean(),
})
.required()
.check((ctx) => {
// Here we may perform complex validation based on any value from the schema
if (ctx.value.input.length > ctx.value.textarea.length) {
ctx.issues.push({
code: 'custom',
message: 'Input text should be shorter than textarea text',
input: ctx.value, // original failed value at schema.safeParse() output for debugging
path: ['input'], // ["foo","bar","baz"] → ctx.value.foo.bar.baz
continue: true, // without "true" check stops
})
ctx.issues.push({
code: 'custom',
message: 'Textarea text should be longer than input text',
input: ctx.value,
path: ['textarea'],
continue: true,
})
}
})
export type InputFormValues = z.input<typeof formPlaygroundSchema>
export type OutputFormValues = z.infer<typeof formPlaygroundSchema>
// useFormPlayground.tsx
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { useFormContext } from 'react-hook-form'
import type { InputFormValues, OutputFormValues } from './formPlaygroundSchema'
/** Returns typed { formMethods } for the form */
export const useFormPlayground = () => {
const formMethods = useFormContext<
InputFormValues,
unknown,
OutputFormValues
>()
return { formMethods }
}
// FormContent.tsx
import { Box } from '@mui/material'
import { useCallback } from 'react'
import type { SubmitHandler } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { useRevalidationOnChangeAfterSubmit } from '@src/shared/libs/react-hook-form/useRevalidationOnChangeAfterSubmit'
import { useFormPlayground } from '../form-config/useFormPlayground'
import type { OutputFormValues } from '../form-config/formPlaygroundSchema'
import {
AutocompleteControlled,
AutocompleteWithFreeTextControlled,
CheckboxControlled,
DateControlled,
InputControlled,
SelectControlled,
RowForInputFields,
ToggleControlled,
} from '@src/shared/components/input-field-controlled'
export const FormContent = (): React.JSX.Element => {
const { t } = useTranslation()
const { formMethods } = useFormPlayground()
useRevalidationOnChangeAfterSubmit()
const onSubmit = useCallback((e: React.FormEvent): void => {
e.preventDefault()
const saveForm: SubmitHandler<OutputFormValues> = async (
data,
// eslint-disable-next-line @typescript-eslint/require-await
): Promise<void> => {
alert(`Saving payload... \n ${JSON.stringify(data, null, 2)}`)
}
const submitHandler = void formMethods.handleSubmit(saveForm)()
return submitHandler
}, [])
const isError = Object.entries(formMethods.formState.errors).length !== 0
return (
<Box
id='playground-form'
component='form'
onSubmit={onSubmit}
sx={{
unset: 'all',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
paddingBottom: '10px',
}}
>
<RowForInputFields width='100%'>
<DateControlled
name='date'
formMethods={formMethods}
legendText={t('label.legendText')}
placeholder='Placeholder'
/>
<InputControlled
name='input'
formMethods={formMethods}
legendText={t('label.legendText')}
placeholder='Placeholder'
/>
</RowForInputFields>
<RowForInputFields width='100%'>
<AutocompleteWithFreeTextControlled
formMethods={formMethods}
name='autocompleteWithFreeText'
legendText={t('label.legendText')}
placeholder='Placeholder'
optionList={['foo', 'bar']}
/>
<AutocompleteControlled
formMethods={formMethods}
name='autocomplete'
legendText={t('label.legendText')}
placeholder='Placeholder'
optionList={['foo', 'bar']}
/>
</RowForInputFields>
<RowForInputFields
height='auto'
width='100%'
>
<InputControlled
formMethods={formMethods}
name='textarea'
legendText={t('label.legendText')}
placeholder='Placeholder'
multiline
rows={3}
/>
</RowForInputFields>
<RowForInputFields width='auto'>
<CheckboxControlled
formMethods={formMethods}
name='checkbox'
legendText={t('label.legendText')}
label={t('label.checkbox')}
/>
<CheckboxControlled
formMethods={formMethods}
name='checkbox'
legendText={t('label.legendText')}
label={t('label.checkbox')}
/>
</RowForInputFields>
<RowForInputFields width='50%'>
<CheckboxControlled
formMethods={formMethods}
name='checkbox'
legendText='legendText'
label={t('label.checkbox')}
/>
<CheckboxControlled
formMethods={formMethods}
name='checkbox'
legendText={t('label.legendText')}
label={t('label.checkbox')}
/>
</RowForInputFields>
<RowForInputFields width='500px'>
<CheckboxControlled
formMethods={formMethods}
name='checkbox'
legendText={t('label.legendText')}
label={t('label.checkbox')}
/>
<CheckboxControlled
formMethods={formMethods}
name='checkbox'
legendText={t('label.legendText')}
label={t('label.checkbox')}
/>
</RowForInputFields>
<RowForInputFields width='auto'>
<SelectControlled
formMethods={formMethods}
name='select'
legendText={t('label.legendText')}
optionList={['option 1', 'option 2', 'option 3']}
/>
</RowForInputFields>
<RowForInputFields>
<ToggleControlled
formMethods={formMethods}
name='toggle'
legendText={t('label.legendText')}
label={t('label.toggle')}
/>
</RowForInputFields>
{isError && (
<Box
sx={{
position: 'relative',
backgroundColor: '#f5f5f5',
padding: 2,
borderRadius: 1,
marginBottom: '20px',
}}
>
<Box
sx={{
position: 'absolute',
top: '5px',
right: '10px',
color: '#2c2c2c',
fontSize: '14px',
}}
>
Form errors
</Box>
<Box component='pre'>
{JSON.stringify(formMethods.formState.errors, null, 2)}
</Box>
</Box>
)}
<Box
sx={{
position: 'relative',
backgroundColor: '#f5f5f5',
padding: 2,
borderRadius: 1,
}}
>
<Box
sx={{
position: 'absolute',
top: '5px',
right: '10px',
color: '#2c2c2c',
fontSize: '14px',
}}
>
Form values
</Box>
<Box component='pre'>
{JSON.stringify(formMethods.watch(), null, 2)}
</Box>
</Box>
</Box>
)
}
// SubmitFormButton.tsx
import { Button } from '@src/shared/components/Button'
import { theme } from '@src/shared/theme'
export const SubmitFormButton = (): React.JSX.Element => {
return (
<Button
form='playground-form'
type='submit'
sx={{
backgroundColor: theme.colors.purple,
}}
>
Submit
</Button>
)
}
// CloseFormIcon.tsx
import { Close } from '@mui/icons-material'
import { IconButton } from '@mui/material'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ConfirmationDialog } from '@src/shared/components/ConfirmationDialog'
import { useFormPlayground } from '../form-config/useFormPlayground'
export const CloseFormIcon = (): React.JSX.Element => {
const { t } = useTranslation()
const [isDialogOpen, setIsDialogOpen] = useState(false)
const { formMethods } = useFormPlayground()
const showCloseConfirmationDialog = (): void => {
setIsDialogOpen(true)
}
const closeModal = (): void => {
alert('Close & navigate up')
}
useEffect(() => {
const closeModalOnEscape = (e: KeyboardEvent): void => {
if (e.key === 'Escape') {
if (formMethods.formState.isDirty) {
showCloseConfirmationDialog()
} else {
closeModal()
}
}
}
document.addEventListener('keydown', closeModalOnEscape)
return (): void => {
document.removeEventListener('keydown', closeModalOnEscape)
}
}, [formMethods.formState.isDirty])
return (
<>
<IconButton
onClick={() => {
if (formMethods.formState.isDirty) {
setIsDialogOpen(true)
} else {
closeModal()
}
}}
>
<Close />
</IconButton>
<ConfirmationDialog
title={t('dialog.confirm')}
text={t('dialog.closeUnsaved')}
isOpen={isDialogOpen}
setIsOpen={setIsDialogOpen}
onSuccess={closeModal}
/>
</>
)
}
// FormPlaygroundModal.tsx
import { ModalLayout } from '@src/shared/layout/ModalLayout'
import { CloseFormIcon } from './widgets/CloseFormIcon'
import { DeleteButton } from './widgets/DeleteButton'
import { FormContent } from './widgets/FormContent'
import { SubmitFormButton } from './widgets/SubmitFormButton'
import { FormPlaygroundProvider } from './form-config/FormPlaygroundProvider'
export const FormPlaygroundModal = (): React.JSX.Element => {
return (
<FormPlaygroundProvider>
<ModalLayout
headerText={'Form playground'}
closeButton={<CloseFormIcon />}
content={<FormContent />}
leftFooterButton={<DeleteButton />}
rightFooterButton={<SubmitFormButton />}
isProgressBar={false}
hideContentForDevPurpose={false}
outlinedContentForDevPurpose={false}
/>
</FormPlaygroundProvider>
)
}