adobe / react-spectrum

A collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.
https://react-spectrum.adobe.com
Apache License 2.0
13k stars 1.13k forks source link

Custom FieldError message overrides the error messages coming from Form's validationErrors #7034

Open ACmaster7 opened 1 month ago

ACmaster7 commented 1 month ago

Provide a general summary of the issue here

I'm using the MyTextField wrapper from the React Aria documentation, and I'm trying to override some of the browser's default error messages. To do this, I pass the errorMessage prop to MyTextField. Additionally, I want to display errors coming from the backend, so I pass the validationErrors prop to the Form component.

Here’s a simplified example:

<Form validationErrors={backendErrors}>
  <MyTextField
    isRequired={true}
    errorMessage={({ validationDetails }) => validationDetails.valueMissing ? 'Please enter a username' : ''}
    {...restOfTheProps}
  />
</Form>

When submitting the form with an empty field, the error message "Please enter a username" displays correctly. However, since the alternative is an empty string, any other validation error (e.g., backend errors) displays nothing.

🤔 Expected Behavior?

The expected behavior is that we should be able to override specific validityState error messages without interfering with other ways the component tries to show error messages, such as backend validation errors. I know that I'm manually overriding the errorMessage the FieldError component is displaying and I might be able to do some workarounds to fix this issue, but it would overcomplicate things, It really shouldn't be this way.

😯 Current Behavior

Checkout the example sandbox I have provided below and do these: First try to submit the form with an empty field and you will see the custom error for valueMissing state, then enter some value (this example will always return an error from the fake backend) and press submit and you'll see that the error from the Form's validationErrors prop won't be displayed, but it will be displayed if you comment the errorMessage prop on MyTextField component

💁 Possible Solution

No response

🔦 Context

No response

🖥️ Steps to Reproduce

This is the sandbox I made so you can see this issue

Version

react-aria-components: 1.3.1

What browsers are you seeing the problem on?

Firefox, Chrome, Safari, Microsoft Edge, Other

If other, please specify.

No response

What operating system are you using?

Windows 11

🧢 Your Company/Team

No response

🕷 Tracking Issue

No response

ACmaster7 commented 1 month ago

Hello?? I think this is a major issue here and I haven't been able to solve it.

There are multiple ways of adding validation to forms in react-aria and they need to be able to be used together.

I customize the native HTML constraint error messages by passing the errorMessage prop like this: errorMessage={({ validationDetails }) => validationDetails.valueMissing ? 'Enter a phone number!' : ''}

I also need complex pattern validation for phone numbers across multiple countries, using the validate prop: validate={value => isValidPhoneNumber(value) ? null : 'Please enter a correct phone number'}

Additionally, on form submission, the server checks if the phone number exists in the database. If not, the server returns an error saying the phone number doesn’t exist. I need to display this server validation error using the validationErrors prop:

<Form
    validationErrors={{ phonenumber: serverErrorForPhonenumber }}
    onSubmit={onSubmit}
 >

The problem is: because I’m customizing the native HTML constraint error messages (as shown in your docs), the FieldError component only shows either "Enter a phone number" or nothing (due to the empty string in the ternary operator).

This prevents any error messages from the server or custom validation logic from being displayed in the FieldError component. Essentially, once I customize the HTML constraint message, all other error messages (whether from the validate prop or validationErrors) are not displayed!

I have already provided a codesandbox example of this issue in my previous message. Thanks

devongovett commented 1 month ago

The original validationErrors are also passed to the FieldError component. Could you do something like this?

<FieldError>
  {({ validationDetails, validationErrors }) => validationDetails.valueMissing ? 'Please enter a username' : validationErrors.join(' ')}
</FieldError>
ACmaster7 commented 1 month ago

Thank you this works! Since I’m using the reusable wrapper for the TextField component from the docs here, I’m passing the errorMessage prop to MyTextField like this:

<Form
   validationErrors={{username: testServerError}}
>
   <MyTextField
      type="text"
      isRequired={true}
      errorMessage={({ validationDetails, validationErrors }) =>
        validationDetails.valueMissing ? 'Please enter a username' : validationErrors.join(' ')}
      validate={value => isValidPattern(value) ? null : 'Please match the required pattern!'}
      label='Username'
      name="username"
    >
</Form>

While this solution works, it feels more like a workaround. Handling this automatically seems like something the library should manage.

Would you consider looking into this further, or do you feel this is the intended approach?

devongovett commented 1 month ago

No this is intended. I don't think we can assume that an empty string means you want to show the default. It might mean you don't want to show that error. This gives you full control. Perhaps we could update the example in the docs though.

ACmaster7 commented 1 month ago

No this is intended. I don't think we can assume that an empty string means you want to show the default. It might mean you don't want to show that error. This gives you full control.

I see your point, but I believe it would be helpful to allow customization of native HTML constraint validation messages separately for each form field, without interfering with the default error handling. Just a suggestion.

Perhaps we could update the example in the docs though.

Yes, the docs could definitely use an update to help clarify this distinction for others as well 😀👌

ACmaster7 commented 1 month ago

Well, I thought the issue was resolved by following your solution, but after further testing, I’ve encountered another problem.

It seems that when the validate prop doesn't return null, it completely disables the native HTML constraint errors! This forces me to write the logic for native constraints, like empty values, inside the validate prop myself.

I first tested your solution with an additional validate prop like this: validate={value => value === 'admin' ? 'Nice Try!' : null} It worked as expected—if the value was 'admin', the field became invalid, and returning null still allowed the valueMissing constraint to trigger.

Then I implemented my actual validation logic: validate={value => !isValidEmailOrPhone(value) ? 'Enter a **valid** email or phone number' : null}

Here’s the full component setup:

<MyTextField
      type="text"
      isRequired={true}
      errorMessage={({ validationDetails, validationErrors }) =>
         validationDetails.valueMissing ? 'Mobile or Email is required!' : validationErrors.join('')}
      validate={value => !isValidEmailOrMobile(value) ? 'Enter a **valid** email or mobile number' : null}
      label='Email or Mobile'
      name="username"
/>

the validate prop will only return null when the value is actually a valid email or mobile number, so in any other case such as an empty value it will still return the string: "Enter a valid email or mobile number". (It shows the "Enter a valid email or mobile number" message even when the field is empty. When I submit the form with an empty field, it should trigger the valueMissing error and return 'Mobile or Email is required!'.)

The whole problem is that I need to customize and have the native HTML constraint error messages (like for isRequired, minLength, etc.) while also being able to display errors for more complex validation logic.

It seems like we can't fully combine these different types of validation as intended. This seems like a somewhat big issue—having to manually handle the missing value (or other native HTML constraints) checks in the validate prop in cases like this.