Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v5] Remove condition restricting error display to touched inputs #9781

Merged
merged 12 commits into from
Apr 18, 2024
2 changes: 1 addition & 1 deletion docs/EditTutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -890,7 +890,7 @@ const AccordionSectionTitle = ({ children, name }) => {

return (
<Typography color={
!formGroupState.isValid && formGroupState.isDirty
!formGroupState.isValid
? 'error'
: 'inherit'
}
Expand Down
28 changes: 10 additions & 18 deletions docs/Inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -877,16 +877,15 @@ import { useController } from 'react-hook-form';
const BoundedTextField = ({ name, label }: { name: string; label: string }) => {
const {
field,
fieldState: { isTouched, invalid, error },
formState: { isSubmitted }
fieldState: { invalid, error }
} = useController({ name, defaultValue: '' });

return (
<TextField
{...field}
label={label}
error={(isTouched || isSubmitted) && invalid}
helperText={(isTouched || isSubmitted) && invalid ? error?.message : ''}
error={invalid}
helperText={invalid ? error?.message : ''}
/>
);
};
Expand Down Expand Up @@ -947,8 +946,7 @@ const BoundedTextField = (props: BoundedTextFieldProps) => {
const { onChange, onBlur, label, ...rest } = props;
const {
field,
fieldState: { isTouched, invalid, error },
formState: { isSubmitted },
fieldState: { invalid, error },
isRequired,
} = useInput({
// Pass the event handlers to the hook but not the component as the field property already has them.
Expand All @@ -962,8 +960,8 @@ const BoundedTextField = (props: BoundedTextFieldProps) => {
<TextField
{...field}
label={label}
error={(isTouched || isSubmitted) && invalid}
helperText={(isTouched || isSubmitted) && invalid ? error?.message : ""}
error={invalid}
helperText={invalid ? error?.message : ""}
required={isRequired}
{...rest}
/>
Expand Down Expand Up @@ -1000,11 +998,7 @@ import { Select, MenuItem } from "@mui/material";
import { InputProps, useInput } from "react-admin";

const SexInput = (props: InputProps) => {
const {
field,
fieldState: { isTouched, invalid, error },
formState: { isSubmitted },
} = useInput(props);
const { field } = useInput(props);

return (
<Select label="Sex" {...field}>
Expand Down Expand Up @@ -1054,8 +1048,7 @@ const BoundedTextField = (props: BoundedTextFieldProps) => {
const { onChange, onBlur, label, helperText, ...rest } = props;
const {
field,
fieldState: { isTouched, invalid, error },
formState: { isSubmitted },
fieldState: { invalid, error },
isRequired,
} = useInput({
onChange,
Expand All @@ -1064,17 +1057,16 @@ const BoundedTextField = (props: BoundedTextFieldProps) => {
});

const renderHelperText =
helperText !== false || ((isTouched || isSubmitted) && invalid);
helperText !== false || invalid;

return (
<TextField
{...field}
label={label}
error={(isTouched || isSubmitted) && invalid}
error={invalid}
helperText={
renderHelperText ? (
<InputHelperText
touched={isTouched || isSubmitted}
error={error?.message}
helperText={helperText}
/>
Expand Down
10 changes: 10 additions & 0 deletions docs/Upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,16 @@ The following components are not affected and can still be used in standalone mo
- `<SimpleList>`
- `<SingleFieldList>`

## Inputs No Longer Require To Be Touched To Display A Validation Error

In previous versions, validation errors were only displayed after the input was touched or the form was submitted. In v5, validation errors are fully entrusted to the form library (`react-hook-form`), which is responsible to decide when to display them.

For most use-cases this will have no impact, because `react-hook-form` works the same way (it will wait for an input to be touched before triggering its validation).

But this should help with some advanced cases, for instance if some validation errors need to be displayed on untouched fields.

It will also improve the user experience, as the form `isValid` state will be consistent with error messages displayed on inputs, regardless of whether they have been touched or not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be useful to remind users of the mode prop with a link to RHF documentation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

## Upgrading to v4

If you are on react-admin v3, follow the [Upgrading to v4](https://marmelab.com/react-admin/doc/4.16/Upgrade.html) guide before upgrading to v5.
14 changes: 4 additions & 10 deletions docs/useInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,7 @@ const BoundedTextField = (props) => {
const { onChange, onBlur, label, helperText, ...rest } = props;
const {
field,
fieldState: { isTouched, invalid, error },
formState: { isSubmitted },
fieldState: { invalid, error },
isRequired
} = useInput({
// Pass the event handlers to the hook but not the component as the field property already has them.
Expand All @@ -80,11 +79,10 @@ const BoundedTextField = (props) => {
<TextField
{...field}
label={label}
error={(isTouched || isSubmitted) && invalid}
helperText={helperText !== false || ((isTouched || isSubmitted) && invalid)
error={invalid}
helperText={helperText !== false || invalid
? (
<InputHelperText
touched={isTouched || isSubmitted}
error={error?.message}
helperText={helperText}
/>
Expand Down Expand Up @@ -119,11 +117,7 @@ import MenuItem from '@mui/material/MenuItem';
import { useInput } from 'react-admin';

const SexInput = props => {
const {
field,
fieldState: { isTouched, invalid, error },
formState: { isSubmitted }
} = useInput(props);
const { field } = useInput(props);

return (
<Select
Expand Down
16 changes: 3 additions & 13 deletions packages/ra-input-rich-text/src/RichTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,6 @@ const RichTextInputContent = ({
error,
helperText,
id,
isTouched,
isSubmitted,
invalid,
toolbar,
}: RichTextInputContentProps) => (
Expand All @@ -279,18 +277,10 @@ const RichTextInputContent = ({
/>
</TiptapEditorProvider>
<FormHelperText
className={
(isTouched || isSubmitted) && invalid
? 'ra-rich-text-input-error'
: ''
}
error={(isTouched || isSubmitted) && invalid}
className={invalid ? 'ra-rich-text-input-error' : ''}
error={invalid}
>
<InputHelperText
touched={isTouched || isSubmitted}
error={error?.message}
helperText={helperText}
/>
<InputHelperText error={error?.message} helperText={helperText} />
</FormHelperText>
</>
);
Expand Down
8 changes: 2 additions & 6 deletions packages/ra-ui-materialui/src/form/FormTabHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Link, useLocation } from 'react-router-dom';
import { Tab as MuiTab, TabProps as MuiTabProps } from '@mui/material';
import clsx from 'clsx';
import { useTranslate, useFormGroup } from 'ra-core';
import { useFormState } from 'react-hook-form';

import { TabbedFormClasses } from './TabbedFormView';

Expand All @@ -21,7 +20,6 @@ export const FormTabHeader = ({
}: FormTabHeaderProps): ReactElement => {
const translate = useTranslate();
const location = useLocation();
const { isSubmitted } = useFormState();
const formGroup = useFormGroup(value.toString());

const propsForLink = {
Expand All @@ -44,10 +42,8 @@ export const FormTabHeader = ({
value={value}
icon={icon}
className={clsx('form-tab', className, {
[TabbedFormClasses.errorTabButton]:
!formGroup.isValid && (formGroup.isTouched || isSubmitted),
error:
!formGroup.isValid && (formGroup.isTouched || isSubmitted),
[TabbedFormClasses.errorTabButton]: !formGroup.isValid,
error: !formGroup.isValid,
})}
{...(syncWithLocation ? propsForLink : {})} // to avoid TypeScript screams, see https://github.com/mui/material-ui/issues/9106#issuecomment-451270521
id={`tabheader-${value}`}
Expand Down
14 changes: 5 additions & 9 deletions packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,6 @@ export const ArrayInput = (props: ArrayInputProps) => {
},
});

const { isSubmitted } = formState;

// We need to register the array itself as a field to enable validation at its level
useEffect(() => {
register(finalSource);
Expand All @@ -142,7 +140,7 @@ export const ArrayInput = (props: ArrayInputProps) => {
fieldArrayInputControl: fieldProps,
});

const { isDirty, error } = getFieldState(finalSource, formState);
const { error } = getFieldState(finalSource, formState);

if (isPending) {
return (
Expand All @@ -151,8 +149,7 @@ export const ArrayInput = (props: ArrayInputProps) => {
</Labeled>
);
}
const renderHelperText =
helperText !== false || ((isDirty || isSubmitted) && !!error);
const renderHelperText = helperText !== false || !!error;

return (
<Root
Expand All @@ -164,14 +161,14 @@ export const ArrayInput = (props: ArrayInputProps) => {
ArrayInputClasses.root,
className
)}
error={(isDirty || isSubmitted) && !!error}
error={!!error}
{...sanitizeInputRestProps(rest)}
>
<InputLabel
htmlFor={finalSource}
className={ArrayInputClasses.label}
shrink
error={(isDirty || isSubmitted) && !!error}
error={!!error}
>
<FieldTitle
label={label}
Expand All @@ -192,9 +189,8 @@ export const ArrayInput = (props: ArrayInputProps) => {
})}
</ArrayInputContext.Provider>
{renderHelperText ? (
<FormHelperText error={(isDirty || isSubmitted) && !!error}>
<FormHelperText error={!!error}>
<InputHelperText
touched={isDirty || isSubmitted}
// root property is applicable to built-in validation only,
// Resolvers are yet to support useFieldArray root level validation.
// Reference: https://react-hook-form.com/docs/usefieldarray
Expand Down
18 changes: 3 additions & 15 deletions packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,7 @@ export const AutocompleteInput = <
id,
field,
isRequired,
fieldState: { error, invalid, isTouched },
formState: { isSubmitted },
fieldState: { error, invalid },
} = useInput({
defaultValue,
id: idOverride,
Expand Down Expand Up @@ -545,10 +544,7 @@ If you provided a React element for the optionText prop, you must also provide t
const isOptionEqualToValue = (option, value) => {
return String(getChoiceValue(option)) === String(getChoiceValue(value));
};
const renderHelperText =
!!fetchError ||
helperText !== false ||
((isTouched || isSubmitted) && invalid);
const renderHelperText = !!fetchError || helperText !== false || invalid;

return (
<>
Expand Down Expand Up @@ -578,18 +574,10 @@ If you provided a React element for the optionText prop, you must also provide t
isRequired={isRequired}
/>
}
error={
!!fetchError ||
((isTouched || isSubmitted) && invalid)
}
error={!!fetchError || invalid}
helperText={
renderHelperText ? (
<InputHelperText
touched={
isTouched ||
isSubmitted ||
fetchError
}
error={
error?.message ||
fetchError?.message
Expand Down
9 changes: 3 additions & 6 deletions packages/ra-ui-materialui/src/input/BooleanInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ export const BooleanInput = (props: BooleanInputProps) => {
id,
field,
isRequired,
fieldState: { error, invalid, isTouched },
formState: { isSubmitted },
fieldState: { error, invalid },
} = useInput({
defaultValue,
format,
Expand All @@ -62,8 +61,7 @@ export const BooleanInput = (props: BooleanInputProps) => {
[field]
);

const renderHelperText =
helperText !== false || ((isTouched || isSubmitted) && invalid);
const renderHelperText = helperText !== false || invalid;

return (
<FormGroup
Expand Down Expand Up @@ -95,9 +93,8 @@ export const BooleanInput = (props: BooleanInputProps) => {
}
/>
{renderHelperText ? (
<FormHelperText error={(isTouched || isSubmitted) && invalid}>
<FormHelperText error={invalid}>
<InputHelperText
touched={isTouched || isSubmitted}
error={error?.message}
helperText={helperText}
/>
Expand Down
15 changes: 4 additions & 11 deletions packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,7 @@ export const CheckboxGroupInput: FunctionComponent<CheckboxGroupInputProps> = pr

const {
field: { onChange: formOnChange, onBlur: formOnBlur, value, ref },
fieldState: { error, invalid, isTouched },
formState: { isSubmitted },
fieldState: { error, invalid },
id,
isRequired,
} = useInput({
Expand Down Expand Up @@ -202,16 +201,13 @@ export const CheckboxGroupInput: FunctionComponent<CheckboxGroupInputProps> = pr
);
}

const renderHelperText =
!!fetchError ||
helperText !== false ||
((isTouched || isSubmitted) && invalid);
const renderHelperText = !!fetchError || helperText !== false || invalid;

return (
<StyledFormControl
component="fieldset"
margin={margin}
error={fetchError || ((isTouched || isSubmitted) && invalid)}
error={fetchError || invalid}
className={clsx('ra-input', `ra-input-${source}`, className)}
{...sanitizeRestProps(rest)}
>
Expand Down Expand Up @@ -246,13 +242,10 @@ export const CheckboxGroupInput: FunctionComponent<CheckboxGroupInputProps> = pr
</FormGroup>
{renderHelperText ? (
<FormHelperText
error={
fetchError || ((isTouched || isSubmitted) && !!error)
}
error={fetchError || !!error}
className={CheckboxGroupInputClasses.helperText}
>
<InputHelperText
touched={isTouched || isSubmitted || fetchError}
error={error?.message || fetchError?.message}
helperText={helperText}
/>
Expand Down
5 changes: 0 additions & 5 deletions packages/ra-ui-materialui/src/input/DatagridInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,6 @@ export const DatagridInput = (props: DatagridInputProps) => {
</>
)}
<InputHelperText
touched={
fieldState.isTouched ||
formState.isSubmitted ||
fetchError
}
error={fieldState.error?.message || fetchError?.message}
/>
</ListContextProvider>
Expand Down
Loading