Closed GProst closed 4 years ago
I actually used to expose ref
for error inputs. Let me think about this, see if it's a good idea to embed in RHF or some alternative solution.
I think we can expose the ref in the errors object, and let user to do the scroll into the view or any other other stuff.
Is anyone already working on this? I would love to work on this if it's available.
no one is working on it just yet, i think the idea is to expose errors object with ref. so you can do a scrollIntoView
and then focus()
.
errors.firstName.ref.scrollntoView();
// then errors.firstName.ref.focus();
you can almost build a custom hook for it.
import useScrollToError from 'useScrollToError';
const { scrollIntoError, regsiterErrorRef } = useScrollToError();
useEffect(() => {
regsiterErrorRef(); // you can register other errors for ref which is not provided. eg server error which field you want to scroll into.
}, [regsiterErrorRef])
useEffect(() => {
scrollIntoError(errors)
}, [errors])
actually we already expose ref inside the error object: https://codesandbox.io/s/nrr4n9p8n4 just need to build that custom hook above ^
@bluebill1049 ref is empty object if an error was set manually via setError
:
https://codesandbox.io/s/react-hook-form-errors-53jjv
yea that's why this part:
useEffect(() => {
regsiterErrorRef(); // you can register other errors for ref which is not provided. eg server error which field you want to scroll into.
}, [regsiterErrorRef])
if you set an error manually there is no ref
we can even support third argument as ref with setError
@bluebill1049 hmm, I assumed that if we set an error manually on the field name which has ref it should use this ref
oh good point, if there ref was registered.
@GProst so if we can fix setError
to associated with error ref, we should be pretty good.
@bluebill1049 yeah, I believe this would solve it for me 👍
👍 should be a pretty easy PR, i will sort it out this weekend for ya
@bluebill1049 awesome, much appreciated ❤️
I'am using a controlled input and don't get input field in ref object. Can you let me know how to do it for controlled input.
@kancharla-sandeep take a look onFocus prop: https://react-hook-form.com/api#Controller
Is the third argument to setError (ref) implemented? And documented somewhere?
Is the third argument to setError (ref) implemented? And documented somewhere?
We don't have a third argument as ref.
can anyone share his workaround if it's done?
can anyone share his workaround if it's done?
function useScrollToError(errors) {
useEffect(() => {
const errorsvalues = Object.values(errors)
if (errorsvalues.length > 0) {
errorsvalues[0].ref.scrollIntoView({ behavior: 'smooth' })
}
}, [errors])
}
can anyone share his workaround if it's done?
function useScrollToError(errors) { useEffect(() => { const errorsvalues = Object.values(errors) if (errorsvalues.length > 0) { errorsvalues[0].ref.scrollIntoView({ behavior: 'smooth' }) } }, [errors]) }
Thanks @vensauro setFocus
is coming, maybe it will be helpful?
am i missing something? errorsvalues[0].ref
is not a DOM element. i've attached the ref
attribute to the element verified that it is correct at this stage. am i supposed to manually manage all my refs? is this for version 7? so little detail...
EDIT: i have determined that i had to upgrade from v6 to v7 to make this work. this is a pretty huge task as v7 has all sorts of breaking changes. :(
Yes, this is for v7, but can be done for v6,
and i used a enhanced version of this in v7:
function useScrollToError(errors) {
useEffect(() => {
for(const error of Object.values(errors)) {
if(error?.ref) {
error.ref.scrollIntoView({ behavior: 'smooth' })
break
}
}
}, [errors])
}
on v6, if using controller, you can use the focus like this:
React.useEffect(() => {
if (errors.firstName) errors.firstName.ref.focus();
}, [errors]);
or you can customize the input, making a shared ref and put an onFocus function on the controller, that make the scroll for the ref, you can pick the ref from the render method, or from the useController return, if you use this ref, this ref will be the html element
Using version 6.13.1 along with yup Note:
const onError = (errors, e) => {
let errorsArray = Object.keys(errors)
if (errorsArray.length) {
let temp = document.getElementById(errorsArray[0])
if (temp) {
//to focus on error -> window.$(`#${errorsArray[0]}`).focus()
window.$('html, body').animate({
scrollTop: window.$(`#${errorsArray[0]}`).offset().top
}, 1000);
}
}
}
<form onSubmit={handleSubmit(onSubmit, onError)}></form>
can anyone share his workaround if it's done?
function useScrollToError(errors) { useEffect(() => { const errorsvalues = Object.values(errors) if (errorsvalues.length > 0) { errorsvalues[0].ref.scrollIntoView({ behavior: 'smooth' }) } }, [errors]) }
Thanks for sharing your code! Where & when should I call the function?
Here's a working version, which is a mix of above solutions:
if (errors) {
const errorsvalues = Object.values(errors)
if (errorsvalues.length > 0) {
let firstErrorElement = document.getElementsByName(errorsvalues[0].ref.name)[0];
firstErrorElement.scrollIntoView({ behavior: `smooth`, block: 'center'});
}
}
setError
has a shouldFocus
option: https://react-hook-form.com/api/useform/seterror
One thing I have noticed is errors
are not in order they are registered but alphabetical. So the first ref that you scroll to may not be the top-most. I'm not sure how one would go about doing that.
function useScrollToError(errors) {
useEffect(() => {
for(const error of Object.values(errors)) {
if(error?.ref) {
error.ref.scrollIntoView({ behavior: 'smooth' })
break
}
}
}, [errors])
}
@eberens-nydig This is going to tricky especially with deeply nested error objects, perhaps you want to manage your own input order. more issues would be occurred with media query and breakpoints when layout shifts.
Here's one that scrolls to the top-most input:
const elements = Object.keys(errors).map(name => document.getElementsByName(name)[0]).filter(el => !!el);
elements.sort((a, b) => b.scrollHeight - a.scrollHeight);
elements[0]?.scrollIntoView({behavior: 'smooth', block: 'center'});
Here's one that scrolls to the top-most input:
const elements = Object.keys(errors).map(name => document.getElementsByName(name)[0]).filter(el => !!el); elements.sort((a, b) => b.scrollHeight - a.scrollHeight); elements[0]?.scrollIntoView({behavior: 'smooth', block: 'center'});
Thanks for sharing. I don't know why, but for me, a.scrollHeight - b.scrollHeight
is the correct thing (not b-a). Maybe because my layout is RTL.
One thing I have noticed is
errors
are not in order they are registered but alphabetical. So the first ref that you scroll to may not be the top-most. I'm not sure how one would go about doing that.There's also another problem: Uncontrolled inputs have higher priority than Controlled inputs and they are scrolled first.
Another annoying thing is when the user correct their input, the input no longer has an error and the form scrolls to the next one! For example, the name input should only have English characters and at least 3 characters long; when a user named "John" types J o h then the input no longer gives an error at it scrolls to the next one.
you can pass the "shouldFocusError" to the form, and then pass the ref to the "input" or "textArea", if the formItem has error, the page will scroll to it and it will be focused, it works to me
https://codesandbox.io/s/react-hooks-form-scroll-to-the-first-error-76bbf for the example above, if you scroll the page to the submit button, and then click the button, the page will scroll to the error input("userName" input)
you can pass the "shouldFocusError" to the form, and then pass the ref to the "input" or "textArea", if the formItem has error, the page will scroll to it and it will be focused, it works to me
https://codesandbox.io/s/react-hooks-form-scroll-to-the-first-error-76bbf for the example above, if you scroll the page to the submit button, and then click the button, the page will scroll to the error input("userName" input)
I'm thinking through how this could work with React Native, where you have to call scrollTo(name)
using React Native Anchor.
@bluebill1049 right now, the handleSubmit
's onInvalid
returns this:
Since I'm a bit new to the internals of react-hook-form, I'm trying to figure out how to parse this.
Should I just recurse the errors
object until I find a ref.name
?
Or does this exist already? it would be nice to do something like this for React Native:
handleSubmit(onSubmit, (errors) => {
const names = getErrorNames(errors)
scrollTo(names[0])
})
Does something like getErrorNames
exist that I could use?
It seems like I want to clone the logic from focusFieldBy
, but just to get the name of the field.
Thanks!
@nandorojo that's an interesting project(https://github.com/nandorojo/react-native-anchors), right now react native does have focus management, however for scroll (animated) into the field then focus is something missing both in terms of web and react-native.
Should I just recurse the errors object until I find a ref.name?
That's pretty much what we have internally.
@bluebill1049 good to know, thanks. I have a Formik example on that project, but I'm migrating to RHF, so I'm trying to see how to apply similar logic.
Another question I have regarding errors: is there a way for me to "test" a given field to check if there is an error?
Something like:
const values = getValues() // { firstName: '' }
const rules = { required: true }
hasErrors(values, { firstName: rules })
I'm building a wrapper around RHF for multi-step forms that you can persist on the server. That way, if a user is on step 4 and refreshes, they return to the same place and the form state rehydrates.
The API looks like this:
One issue I'm having: in order to set the initial step, I need to find the first step that has errored / invalid fields.
Currently, I'm just checking if any of the items in fields
is undefined or an empty string.
However, I'd like to do something like this:
const values = getValues()
const firstInvalidStepIndex = steps.findIndex(step => {
const doesStepHaveErrors = Object.entries(step.fields).some(([fieldPath, fieldRules]) => {
const isError = hasErrors(values, { [fieldPath]: fieldRules }) // does hasErrors exist?
return isError
})
return doesStepHaveErrors
})
setStepIndex(firstInvalidStepIndex)
Is there a function like hasErrors
that I can use, which takes in the values and the rules for specific fields and tells me if there is an error?
Thanks again!
@nandorojo What about split your step into multiple form (each has its own useForm)? Would that be an easier solution than having a single useForm for the entire form application?
Another question I have regarding errors: is there a way for me to "test" a given field to check if there is an error?
validate
function, so you can test aginst individual function.What about split your step into multiple form (each has its own useForm)? Would that be an easier solution than having a single useForm for the entire form application?
This was my initial intuition too. However, it doesn't exactly fit my situation for 2 reasons:
email
) relies on Step 1 (firstName
), since it shows a text like "Hey firstName
, welcome to app
".post('/user')
). During each step before that, we are dealing with an incomplete user. So splitting it into pieces wouldn't exactly work, since the backend needs to receive a complete user at the end. In the intermediate steps, I am storing a userDraft
in the backend, which is simply a JSON blob of the current form state.Hope that clears things up!
if you are using build in, you probably have to use validate function, so you can test aginst individual function.
I think this is exactly what I needed. However, I don't think validate
is exported from react-hook-form
. Where can I access it?
I would store valid data in a state management library (many out there), I think that's what they r good at. (good use case)
I would run a schema validation (possible to share with your server) to validate the entire payload. In fact, you can have an extra state to capture the step validation as well.
I think this is exactly what I needed. However, I don't think validate is exported from react-hook-form. Where can I access it?
// i was refer to expose those function into individual validate functions
const testFunction = (value) => {
return !!value;
}
<input {...register({ validate: testFunction })} />
I would store valid data in a state management library (many out there), I think that's what they r good at. (good use case)
Yeah I agree. The thing is, then I can't take advantage of all the benefits of RHF, like render performance and validation for each step. I already have the multi-step form working really well with RHF. The only missing piece is validating fields outside of the form itself.
I would run a schema validation (possible to share with your server) to validate the entire payload. In fact, you can have an extra state to capture the step validation as well.
Yeah I'll probably do this at the last step. However, for intermediate steps, it would be helpful to use the rules
API from Controller
. Something like:
import { validateRules } from 'react-hook-form'
const rules = {
firstName: {
required: true
}
}
const errors = validateRules(getValues(), rules)
I could use something like yup
with the resolver too, but I prefer to use rules
inside of Controller
, since I want to partially validate at each step. That is to say, I only want to check for errors on fields that are rendered.
If you're at step 3, I don't want to check if step 4 has errors yet. If I use the yup
resolver at the root useForm
, then it validates the entire form, as opposed to only the form up to a current step.
I know it's a pretty unique use-case, and if there's nothing like validateRules
, I can still make it work with a custom validate function, like you mentioned. But figured I'd pass along what I was trying out. Happy to share my code for multi-step forms too once I finish it up.
For what it's worth, I'm a bit new to React Hook Form, so I appreciate you walking me through the options.
Thanks again @bluebill1049!
@nandorojo this looks awesome!
Yeah I agree. The thing is, then I can't take advantage of all the benefits of RHF, like render performance and validation for each step. I already have the multi-step form working really well with RHF. The only missing piece is validating fields outside of the form itself.
by the way, I meant only submit valid data into your store, so really only on handleSubmit
.
const elements = Object.keys(errors).map(name => document.getElementsByName(name)[0]).filter(el => !!el);
elements.sort((a, b) => b.scrollHeight - a.scrollHeight);
elements[0]?.scrollIntoView({behavior: 'smooth', block: 'center'});
above doesn't;t works in case form as neasted fields
I have a huge form with nested fields in the form sample example of neted fields in form
address:{
addressLine1:'some address',
city:'some city'
state:' some state'
}
for showing error message i have used span element eg: <span class="error-container" style="color: red;">This field is required</span>
and used below function for scrolling to error field it works for nested field form
export const useScrollToError = (errors:any, errorEleClass:string) => {
useEffect(() => {
if (Object.keys(errors)?.length > 0) {
const container = document.querySelector(`.${errorEleClass}`);
container?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, [errors])
}
Can anyone tell me how to focus on error with Material UI and React Hook Form with Controller ?
<Controller
control={control}
name="name"
render={({ field, fieldState }) => (
<TextField
required
label={t('main.performer_name')}
{...field}
id="name"
disabled={translationFields !== undefined}
fullWidth
{...textFieldError(fieldState.error)}
/>
)}
/>
react-hook-version : 7.34.2
and zod
as a validation
I have checked setFocus
and error.ref.focus()
or error.ref.scrollIntoView({})
but not helped
Can anyone tell me how to focus on error with Material UI and React Hook Form with Controller ?
<Controller control={control} name="name" render={({ field, fieldState }) => ( <TextField required label={t('main.performer_name')} {...field} id="name" disabled={translationFields !== undefined} fullWidth {...textFieldError(fieldState.error)} /> )} />
react-hook-version : 7.34.2
andzod
as a validation I have checkedsetFocus
anderror.ref.focus()
orerror.ref.scrollIntoView({})
but not helped
@ruslan-bairamovi-axon
Can you try to pass the ref
as inputRef
. As MUI TextField will pass ref
to FormControl
which is the div
wrapper rather than the lable
or input
, hence .focus()
might not work there.
<Controller
control={control}
name="name"
render={({ field: {ref, ...restField}, fieldState }) => (
<TextField
// Other props...
{...restField}
inputRef={ref}
/>
)}
/>
Here's one that scrolls to the top-most input:
const elements = Object.keys(errors).map(name => document.getElementsByName(name)[0]).filter(el => !!el); elements.sort((a, b) => b.scrollHeight - a.scrollHeight); elements[0]?.scrollIntoView({behavior: 'smooth', block: 'center'});
For some reason, in my form, all inputs have the same x.scrollHeight
, x.offsetHeight
, x.scrollTop
, x.clientHeight
and x.clientTop
; so I used x.getBoundingClientRect().top
to sort them correctly.
Thanks to all insights from people participating in this issue, I was able to handle this for onSubmit
mode.
With this approach, every time the user hits the submit button, the form will scroll to the first element of elements with validation error and focus it. The next "scroll & focus" will happen after the user hits the submit button again. You can play with useForm
mode
s and tweak the functions to achieve what you want.
// setting shouldFocusError to false so we can have our custom scroll function
const methods = useForm({ shouldFocusError: false })
// using a state here to make the "scroll & focus" happen once per submission
const [canFocus, setCanFocus] = useState(true)
const onError = () => {
setCanFocus(true)
}
useEffect(() => {
if (methods.formState.errors && canFocus) {
// Sort inputs based on their position on the page. (the order will be based on validaton order otherwise)
const elements = Object.keys(methods.formState.errors)
.map((name) => document.getElementsByName(name)[0])
.filter((el) => !!el);
elements.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
if (elements.length > 0) {
let errorElement = elements[0];
errorElement.scrollIntoView({ behavior: "smooth", block: "center" }); // scrollIntoView options are not supported in Safari
errorElement.focus({ preventScroll: true });
setCanFocus(false) // so the form doesn't suddenly jump to the next input that has error.
}
}
}, [methods.formState, canFocus]);
return(
<form submit={methods.handleSubmit(onSubmit, onError)}>
...
</form>
)
It would be nice to have some
scrollToError
method which would scroll to the highest input found which name matches one of the errors keys.Currently, there is a similar behavior that happens after the form validation but there are cases when errors are set manually for some fields using
setError
API, for example, after server's error response.I believe I could implement such functionality outside RHF but I'd have to store input refs and names separately for this and it just feels like it would be much better if it was implemented in RHF.
Or perhaps, alternatively, if I could access all input refs mapped to fields names from RHF it would make it simpler to implement such scrolling functionality manually.