-
-
Save dphrag/4db3b453e02567a0bb52592679554a5b to your computer and use it in GitHub Desktop.
import React from 'react'; | |
import { connect } from 'formik'; | |
class ErrorFocus extends React.Component { | |
componentDidUpdate(prevProps) { | |
const { isSubmitting, isValidating, errors } = prevProps.formik; | |
const keys = Object.keys(errors); | |
if (keys.length > 0 && isSubmitting && !isValidating) { | |
const selector = `[id="${keys[0]}"]`; | |
const errorElement = document.querySelector(selector); | |
errorElement.focus(); | |
} | |
} | |
render() { | |
return null; | |
} | |
} | |
export default connect(ErrorFocus); |
And it is nicer to have the label in view alongside the input. You'll have to change the querySelector to something like:
document.querySelector(`label[for="${keys[0]}"]`);
For a more complete example, you can see my blog post with a Formik example at the end (https://robinvdvleuten.nl/blog/scroll-a-react-component-into-view/).
This is great, thanks... Sometimes I've had large forms and chosen to do some nesting of the data. The address inputs might have a name of address.addressLine1
... etc. You can support nested fields by using this function to flatten the errors object:
const keyify = (obj: { [key: string]: any }, prefix = ''): Array<string> =>
Object.keys(obj).reduce((res, el) => {
if (Array.isArray(obj[el])) {
return res
}
if (typeof obj[el] === 'object' && obj[el] !== null) {
return [...res, ...keyify(obj[el], `${prefix}${el}.`)]
}
return [...res, prefix + el]
}, [] as Array<string>)
And then calling that instead of Object.keys:
...
const keys = keyify(errors)
if (keys.length > 0 && isSubmitting && !isValidating) {
const selector = `[id="${keys[0]}"]`
...
An updated Typescript version with a support for nested fields:
(nesting is taken from here)
(this is a PoC code)import {connect, FormikContextType} from "formik"; import {Component} from "react"; interface IProps { formik: FormikContextType<any>; } class ErrorFocusInternal extends Component<IProps> { public componentDidUpdate(prevProps: IProps) { const {isSubmitting, isValidating, errors} = prevProps.formik; const keyify = (obj, prefix = '') => Object.keys(obj).reduce((res, el) => { if( Array.isArray(obj[el]) ) { return res; } else if( typeof obj[el] === 'object' && obj[el] !== null ) { return [...res, ...keyify(obj[el], prefix + el + '.')]; } else { return [...res, prefix + el]; } }, []); const keys = keyify(errors); if (keys.length > 0 && isSubmitting && !isValidating) { const selector = `[name="${keys[0]}"]`; const errorElement = document.querySelector(selector) as HTMLElement; if (errorElement) { errorElement.focus(); } } } public render = () => null; } export const ErrorFocus = connect<{}>(ErrorFocusInternal);
For some reason I don't understand, when I run this logic, the nested object keys are not ordered.
Have you experienced this?
Everyone who want's to fix bug, when it focus only in first form element, just paste this part of code const selector =
[id^="${keys[0]}"];
For my hook friends:
Based on @ulitiy solution.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>
Thanks to the guys above. 👍
Thanks!! its working fine..
(1/3)Snippet to getting first error(works even if nested case)
import { isObject } from "lodash";
export const getFirstErrorKey = (object: any, keys: string[] = []): any => {
let firstErrorKey = "";
if (Array.isArray(object)) {
for (let i = 0; i < object.length; i++) {
if (object[i]) {
firstErrorKey = Object.keys(object)[i];
break;
}
}
} else {
firstErrorKey = Object.keys(object)[0];
}
if (firstErrorKey && isObject(object[firstErrorKey])) {
return getFirstErrorKey(object[firstErrorKey], [...keys, firstErrorKey]);
}
return [...keys, firstErrorKey].join(".");
};
(2/3)Snippet to focus to that error input
import { getFirstErrorKey } from "./getFirstErrorKey";
export const focusElement = (errors: any) => {
let element = null;
const firstErrorKey = getFirstErrorKey(errors);
if (global.window.document.getElementsByName(firstErrorKey).length) {
element = global.window.document.getElementsByName(firstErrorKey)[0];
if (element instanceof HTMLInputElement) {
element.focus();
} else {
element = element.getElementsByTagName("input")[0];
if (element instanceof HTMLInputElement) {
element.focus();
}
}
}
};
(3/3)Final Usage
useEffect(() => {
if (!isValid && submitCount > 0) {
focusElement(errors);
}
}, [submitCount, isValid]);
Amazing guys! Thanks to all of you! :)
Quick note for those of you who use smooth scroll and are not getting the offset correctly,
I used this:
const pos = errorElement.style.position;
const top = errorElement.style.top;
errorElement.style.position = 'relative';
errorElement.style.top = '-100px';
errorElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
errorElement.style.top = top;
errorElement.style.position = pos;
This is great :) (using the react hooks version), I don't think it works with react-select elements though
All good but not working with react-select
anybody able to fix for react-select?
I used this for ReactSelect :
import React, { useEffect } from 'react';
import { useFormikContext } from 'formik';
const ScrollToFieldError = ({ scrollBehavior = { behavior: 'smooth', block: 'center' } }) => {
const { submitCount, isValid, errors } = useFormikContext();
useEffect(() => {
if (isValid) return;
const fieldErrorNames = getFieldErrorNames(errors);
let element;
if (fieldErrorNames[0].includes('.type')) {
element = document.querySelector(`input[aria-label='${fieldErrorNames[0]}']`);
} else {
element = document.querySelector(`input[name='${fieldErrorNames[0]}']`);
}
if (!element) return;
// Scroll to first known error into view
element.scrollIntoView(scrollBehavior as any);
// Formik doesn't (yet) provide a callback for a client-failed submission,
// thus why this is implemented through a hook that listens to changes on
// the submit count.
}, [submitCount]);
return null;
};
Put ScrollToFieldError within formiks Form.
Pass aria-label props in ReactSelect .
Easiest way to have smooth scrolling is by just utilizing
Element.scrollIntoView()