Reputation: 139
I'm trying to adapt the code below to use react useRef
as opposed to using document.querySelector(selector) as HTMLElement;
as it's not the best practice in react. I'm trying to achieve functionality that scrolls to the first error on a Formik form, this code does work but how I can do this with React useRef
instead?
here is the code:
import React, { useEffect } from 'react';
import { useFormikContext } from 'formik';
const FocusError = () => {
const { errors, isSubmitting, isValidating } = useFormikContext();
useEffect(() => {
if (isSubmitting && !isValidating) {
let keys = Object.keys(errors);
if (keys.length > 0) {
const selector = `[name=${keys[0]}]`;
const errorElement = document.querySelector(selector) as HTMLElement;
if (errorElement) {
errorElement.focus();
}
}
}
}, [errors, isSubmitting, isValidating]);
return null;
};
export default FocusError;
//Put it within formiks Form.
<Formik ...>
<Form>
...
<FocusError />
</Form>
</Formik>
Upvotes: 6
Views: 16266
Reputation: 1
type ScrollToErrorsProps = {
name?: string;
id?: string | any;
formik?: any;
};
export default class FocusError extends React.Component<ScrollToErrorsProps>{
componentDidUpdate(prevProps) {
const { name, id, formik } = this.props;
if (this.props.formik.submitCount > 0 && this.props.formik.isValid === false) {
if (Object.keys(this.props.formik.errors)[0] === this.props.name) {
const el = document.getElementById(this.props.id);
if (el) {
el.scrollIntoView({behavior: "smooth", block: "center", inline: "nearest"});
}
}
}
}
render() {
return null;
}
}
Upvotes: 0
Reputation: 4268
Here is a slight update to 4nduril's answer.
This variation:
focus
instead of only using scrollIntoView
(better accessibility - see https://webaim.org/techniques/formvalidation/). Note that scrolling (including smooth scrolling) is still available in addition to setting focus.usePrevious
(https://ahooks.js.org/hooks/use-previous/) to only set focus to the invalid field after submitting has transitioned from true to false. This avoids the limitation where Formik's validateOnBlur
has to be set to true in 4nduril's answer.export const useFocusInvalidField = <
T extends HTMLElement,
U extends HTMLElement,
>(
name: string,
smoothScroll?: boolean,
) => {
const formikContext = useFormikContext();
// The input/select itself, this is what we will be focusing.
const fieldRef = useRef<T>(null);
// The container element that contains both the input/select and its label.
// This is what we will be scrolling into view. Doing this helps guarantee that
// the label will be visible. If the submit button is at the bottom of the form and the
// label is above the invalid input, _just_ focusing the input could leave the
// label offscreen (above the viewport) which can make it difficult for the user to
// identify the invalid field.
const containerElementRef = useRef<U>(null);
const wasSubmitting = usePrevious(formikContext.isSubmitting);
useEffect(() => {
const firstError = Object.keys(formikContext.errors)[0];
if (
wasSubmitting === true &&
!formikContext.isSubmitting &&
firstError === name &&
fieldRef.current !== null
) {
// Use a helper function from https://github.com/oaf-project/oaf-side-effects.
// This does some browser compatibility and accessibility work for us.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
focusAndScrollIntoViewIfRequired(
fieldRef.current, // focus the input/select
containerElementRef.current ?? fieldRef.current, // scroll the container (and label) into view if set, otherwise scroll to the input
smoothScroll, // optionally use smooth scroll. Accessibility note: this will ignore smooth scroll if the user has indicated a preference for reduced motion.
);
}
}, [
wasSubmitting,
formikContext.isSubmitting,
name,
formikContext.errors,
smoothScroll,
]);
return { fieldRef, containerElementRef };
};
Upvotes: 2
Reputation: 28064
There are already some good answers here, but I ended up taking a slightly different route that some might find appealing.
// in my ScrollToError.tsx file...
import { useFormikContext } from 'formik';
import React, { useEffect } from 'react';
export function ScrollToError() {
const formik = useFormikContext();
const submitting = formik?.isSubmitting;
useEffect(() => {
const el = document.querySelector('.Mui-error, [data-error]');
(el?.parentElement ?? el)?.scrollIntoView();
}, [submitting]);
return null;
}
// later inside my <Formik/> component...
<ScrollToError/>
This finds the first .Mui-error element (typically the label for the field in question) and focuses its parent, or itself if no parent. In some cases, I add an <Alert severity="error"/>
component to display a general error message, which this also finds if I tag it with the data-error attribute. The scroll only happens when isSubmitting
changes.
Upvotes: 19
Reputation: 21
i am using useFormik and made useEffect hook like this.
const formik = useFormik({
initialValues: initialValues,
validationSchema: validators,
onSubmit: (values, {setErrors}) => {
....
}
});
useEffect(() => {
if (!formik.isSubmitting) return;
if (Object.keys(formik.errors).length > 0) {
document.getElementsByName(Object.keys(formik.errors)[0])[0].focus();
}
}, [formik]);
and it set focus to first invalid input field. i hope this helps others.
Upvotes: 1
Reputation: 379
My solution was to build it into my custom form fields. I use MaterialUI TextFields but you can do it of course without. The relevant Code:
import { TextField as MuiTextField } from '@material-ui/core'
export const TextField = ({ name, ...rest }) => {
const [field, meta] = useField({ name, type: 'text' })
const formikBag = useFormikContext()
const fieldRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const firstError = Object.keys(formikBag.errors)[0]
if (formikBag.isSubmitting && firstError === name) {
fieldRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
}, [meta.error, formikBag.isSubmitting, name, formikBag.errors])
return (
<MuiTextField
id={field.name}
ref={fieldRef}
error={meta.touched && Boolean(meta.error)}
helperText={meta.touched && meta.error}
{...rest}
{...field}
/>
)
}
You can even extract it to a custom hook like this:
import { useFormikContext } from 'formik'
import { useRef, useEffect, MutableRefObject } from 'react'
export const useScrollToInvalidField = <T extends HTMLElement>(name: string): MutableRefObject<T | null> => {
const formikBag = useFormikContext()
const fieldRef = useRef<T>(null)
useEffect(() => {
const firstError = Object.keys(formikBag.errors)[0]
if (formikBag.isSubmitting && firstError === name) {
fieldRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
}, [formikBag.isSubmitting, name, formikBag.errors])
return fieldRef
}
Then you just give it the correct Element type of your field (e.g. HTMLDivElement
) and attach the ref to the form field component.
Upvotes: 1
Reputation: 2759
Instead of doing -
errorElement.focus();
Do this -
errorElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
errorElement.focus({ preventScroll: true });
Upvotes: 6