import * as React from 'react'; import { useController, UseControllerProps, useFormState, } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; import polyglotI18nProvider from 'ra-i18n-polyglot'; import englishMessages from 'ra-language-english'; import fakeRestDataProvider from 'ra-data-fakerest'; import { Route, Routes, useNavigate, Link, HashRouter, useLocation, } from 'react-router-dom'; import { CoreAdminContext } from '../core'; import { EditBase, RecordContextProvider, SaveContextProvider, } from '../controller'; import { DeleteButton } from '../test-ui/DeleteButton'; import { Form, FormProps } from './Form'; import { useInput } from './useInput'; import { required, ValidationError } from './validation'; import { mergeTranslations } from '../i18n'; import { I18nProvider, RaRecord } from '../types'; import { TestMemoryRouter } from '../routing'; import { useNotificationContext } from '../notification'; export default { title: 'ra-core/form/Form', }; const Input = props => { const { field, fieldState } = useInput(props); return (
{fieldState.error && fieldState.error.message ? ( ) : null}
); }; const SubmitButton = () => { const { dirtyFields } = useFormState(); // useFormState().isDirty might differ from useFormState().dirtyFields (https://github.com/react-hook-form/react-hook-form/issues/4740) const isDirty = Object.keys(dirtyFields).length > 0; return ( ); }; export const Basic = () => { const [submittedData, setSubmittedData] = React.useState(); return (
setSubmittedData(data)} record={{ id: 1, field1: 'bar', field6: null }} > v || undefined} />
{JSON.stringify(submittedData, null, 2)}
); }; const CustomInput = (props: UseControllerProps) => { const { field, fieldState } = useController(props); return (

{fieldState.error?.message}

); }; export const SanitizeEmptyValues = () => { const [submittedData, setSubmittedData] = React.useState(); const field11 = { name: 'field11' }; const field12 = { name: 'field12', defaultValue: 'bar', }; const field13 = { name: 'field13', defaultValue: '', }; const field14 = { name: 'field14' }; const field16 = { name: 'field16' }; return (
setSubmittedData(data)} record={{ id: 1, field1: 'bar', field6: null, field11: 'bar', field16: null, }} sanitizeEmptyValues > v || undefined} />
                {JSON.stringify(submittedData, null, 2)}
            
); }; export const NullValue = () => { const [result, setResult] = React.useState(); return (
setResult(data)}>
{JSON.stringify(result, null, 2)}
); }; export const UndefinedValue = () => { const [result, setResult] = React.useState(); return (
setResult(data)}>
{JSON.stringify(result, null, 2)}
); }; const defaultI18nProvider = polyglotI18nProvider(() => mergeTranslations(englishMessages, { app: { validation: { required: 'This field must be provided', }, }, }) ); export const FormLevelValidation = ({ i18nProvider = defaultI18nProvider, }: { i18nProvider?: I18nProvider; }) => { const [submittedData, setSubmittedData] = React.useState(); return (
setSubmittedData(data)} record={{ id: 1, field1: 'bar', field6: null }} validate={(values: any) => { const errors: any = {}; if (!values.defaultMessage) { errors.defaultMessage = 'ra.validation.required'; } if (!values.customMessage) { errors.customMessage = 'This field is required'; } if (!values.customMessageTranslationKey) { errors.customMessageTranslationKey = 'app.validation.required'; } if (!values.missingCustomMessageTranslationKey) { errors.missingCustomMessageTranslationKey = 'app.validation.missing'; } return errors; }} >
{JSON.stringify(submittedData, null, 2)}
); }; export const InputLevelValidation = ({ i18nProvider = defaultI18nProvider, }: { i18nProvider?: I18nProvider; }) => { const [submittedData, setSubmittedData] = React.useState(); return (
setSubmittedData(data)} record={{ id: 1, field1: 'bar', field6: null }} >
{JSON.stringify(submittedData, null, 2)}
); }; const zodSchema = z.object({ defaultMessage: z.string(), //.min(1), customMessage: z.string({ error: 'This field is required' }), customMessageTranslationKey: z.string({ error: 'app.validation.required' }), missingCustomMessageTranslationKey: z.string({ error: 'app.validation.missing', }), }); export const ZodResolver = ({ i18nProvider = defaultI18nProvider, }: { i18nProvider?: I18nProvider; }) => { const [result, setResult] = React.useState(); return (
setResult(data)} resolver={zodResolver(zodSchema)} >
{JSON.stringify(result, null, 2)}
); }; type Schema = z.infer; export const ZodResolverWithSchema = ({ i18nProvider = defaultI18nProvider, }: { i18nProvider?: I18nProvider; }) => { const [result, setResult] = React.useState(); return ( record={{}} onSubmit={data => setResult(data)} resolver={zodResolver(zodSchema)} >
{JSON.stringify(result, null, 2)}
); }; const yupSchema = yup.object({ defaultMessage: yup.string().required(), customMessage: yup.string().required('This field is required'), customMessageTranslationKey: yup .string() .required('app.validation.required'), missingCustomMessageTranslationKey: yup .string() .required('app.validation.missing'), }); export const YupResolver = ({ i18nProvider = defaultI18nProvider, }: { i18nProvider?: I18nProvider; }) => { const [result, setResult] = React.useState(); return (
setResult(data)} resolver={yupResolver(yupSchema)} >
{JSON.stringify(result, null, 2)}
); }; const FormUnderTest = () => { const navigate = useNavigate(); return ( <>
setTimeout(() => navigate('/'), 0)} warnWhenUnsavedChanges >
Leave the form ); }; export const WarnWhenUnsavedChanges = ({ i18nProvider = defaultI18nProvider, }: { i18nProvider?: I18nProvider; }) => ( Go to form} /> } /> ); export const InNonDataRouter = ({ i18nProvider = defaultI18nProvider, }: { i18nProvider?: I18nProvider; }) => ( Go to form} /> } /> ); const PostEditWithDelete = ({ mutationMode, }: { mutationMode: 'undoable' | 'optimistic' | 'pessimistic'; }) => (
); /** * Edit a field (making the form dirty), then click Delete. The record is * deleted and the app redirects to the list, but the unsaved-changes blocker * pops a spurious "unsaved changes" confirm dialog even though the record is * already gone. */ export const WarnWhenUnsavedChangesWithDelete = ({ i18nProvider = defaultI18nProvider, mutationMode = 'undoable', }: { i18nProvider?: I18nProvider; mutationMode?: 'undoable' | 'optimistic' | 'pessimistic'; }) => ( } /> Post list

} />
); WarnWhenUnsavedChangesWithDelete.argTypes = { mutationMode: { options: ['undoable', 'optimistic', 'pessimistic'], control: { type: 'select' }, }, }; const Notifications = () => { const { notifications } = useNotificationContext(); return (
    {notifications.map(({ message }, id) => (
  • {message}
  • ))}
); }; export const ServerSideValidation = () => { const save = React.useCallback(values => { const errors: any = {}; if (!values.defaultMessage) { errors.defaultMessage = 'ra.validation.required'; } if (!values.customMessage) { errors.customMessage = 'This field is required'; } if (!values.customMessageTranslationKey) { errors.customMessageTranslationKey = 'app.validation.required'; } if (!values.missingCustomMessageTranslationKey) { errors.missingCustomMessageTranslationKey = 'app.validation.missing'; } if (!values.customGlobalMessage) { errors.customGlobalMessage = 'ra.validation.required'; errors.root = { serverError: 'There are validation errors. Please fix them.', }; } return Object.keys(errors).length > 0 ? errors : undefined; }, []); return (
); }; export const MultiRoutesForm = ({ url, initialRecord, defaultValues, }: { url?: any; initialRecord?: Partial; defaultValues?: Partial; }) => ( } /> ); MultiRoutesForm.args = { url: 'unmodified', initialRecord: 'none', }; MultiRoutesForm.argTypes = { url: { options: [ 'unmodified', 'modified with location state', 'modified with location search', ], mapping: { unmodified: '/form/general', 'modified with location state': { pathname: '/form/general', state: { record: { body: 'from-state' } }, }, 'modified with location search': `/form/general?source=${encodeURIComponent(JSON.stringify({ body: 'from-search' }))}`, }, control: { type: 'select' }, }, defaultValues: { options: ['none', 'provided'], mapping: { none: undefined, provided: { category: 'default category', }, }, control: { type: 'select' }, }, initialRecord: { options: ['none', 'provided'], mapping: { none: undefined, provided: { title: 'lorem', body: 'unmodified' }, }, control: { type: 'select' }, }, }; const FormWithSubRoutes = (props: Partial) => { return (
); }; const TabbedForm = () => { const location = useLocation(); return ( <>
General Settings
); }; const Tab = ({ children, name, }: { children: React.ReactNode; name: string; }) => { const location = useLocation(); return (
{children}
); };