-
-
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); |
Thank you so much! It works nice, but when the error is from the server, and is sent by the client, the focus does not work.
to make it safe just add a condition to check if the element is exist
if (errorElement) {
errorElement.focus();
}
and more safe when you check name
instead of id
const selector = `[name="${keys[0]}"]`;
Thanks a lot @dphrag!!
and thanks @omarkhatibco, it works better with your suggestions.
Thank you!!! Works great!
Thanks mate!
Typescript version
import {connect, FormikContext} from "formik";
import {Component} from "react";
interface IProps {
formik: FormikContext<any>;
}
class ErrorFocusInternal extends Component<IProps> {
public componentDidUpdate(prevProps: IProps) {
const {isSubmitting, isValidating, errors} = prevProps.formik;
const keys = Object.keys(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);
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 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. 👍
thank you for the hooks version!!!
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);
Take note that nesting keys from here don't include an array
Below is the same code as updated by Patrick-Ullrich I have added code to scroll with smooth animation. and i have added another condition if name condition is not found then pick it up be id. any html element will work.
I was using React Fabric/Fluent UI.
import * as React from 'react';
import { useFormikContext } from 'formik';
export const HCCSFocusError = () => {
const { errors, isSubmitting, isValidating } = useFormikContext();
React.useEffect(() => {
if (isSubmitting && !isValidating) {
let keys = Object.keys(errors);
if (keys.length > 0) {
const selector = `[name=${keys[0]}]`;
let errorElement = null;
errorElement = document.querySelector(selector) as HTMLElement;
if (!errorElement) {
errorElement = document.getElementById(keys[0]);
}
const yOffset = -130;
const y = errorElement.getBoundingClientRect().top + window.pageYOffset + yOffset;
const isSmoothScrollSupported = "scrollBehavior" in document.documentElement.style;
if (isSmoothScrollSupported) {
// not on IE/Edge
window.scrollTo({ top: y, behavior: "smooth" });
} else {
// for Edge
document.body.scrollTop = y;
// use an offset to compensate for the fixed header
document.documentElement.scrollTop = y;
}
setTimeout(() => {
errorElement.focus();
}, 500);
}
}
}, [errors, isSubmitting, isValidating]);
return null;
};
Easiest way to have smooth scrolling is by just utilizing Element.scrollIntoView()
import { useEffect } from "react"
import { useFormikContext } from "formik"
const ErrorFocus = () => {
const { isSubmitting, isValidating, errors } = useFormikContext()
useEffect(() => {
const keys = Object.keys(errors);
if (keys.length > 0 && isSubmitting && !isValidating) {
const errorElement = document.querySelector(`[id="${keys[0]}"]`);
if (errorElement) {
errorElement.scrollIntoView({ behavior: "smooth" });
}
}
}, [isSubmitting, isValidating, errors])
return null
}
export default 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 .
this only scrolls to the first field that fails validation, if the keys in
errors
match the order of fields in the form