Skip to content

Instantly share code, notes, and snippets.

@osv
Last active March 4, 2019 07:24
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save osv/691efa00784def6f6004d31cd6108f56 to your computer and use it in GitHub Desktop.
Save osv/691efa00784def6f6004d31cd6108f56 to your computer and use it in GitHub Desktop.
React Formik, scroll to first element in DOM that is not valid and has error (Hook + Typescript version)
import { connect, FormikContext, getIn } from 'formik';
import * as React from 'react';
import { useEffectWhenCountIncremented } from './reactHooks';
import { PageContext } from './context';
const FormikGotoErrorOnSubmitEffectFn: React.FunctionComponent<{
formik?: FormikContext<any>;
}> = ({ formik }) => {
const { openAllSections } = React.useContext(PageContext);
const validateFinishedCount =
formik && // if component is in React formik's context
formik.isSubmitting && // and submitting
!formik.isValidating && // and validation is finished
Object.keys(formik.errors) && // and has errors
formik.submitCount; // than return submit count
useEffectWhenCountIncremented(validateFinishedCount, () => {
const { errors } = formik!;
// Uncollapse sections. In this example used context which cause rerendering this component,
// that is why used useEffectWhenCountIncremented
openAllSections();
setTimeout(() => {
// Find first element which id or name is path to error and scroll to it
// tslint:disable:no-string-literal
const errorElement = Array.from(document.querySelectorAll('form [id], form [name]')).find(
el =>
(el.id && (getIn(errors, el.id) || getIn(errors, el.id.replace('_', '.')))) ||
(el['name'] && getIn(errors, el['name'])),
);
if (errorElement) {
errorElement.scrollIntoView({ behavior: 'smooth' });
if (errorElement['focus']) {
setTimeout(() => {
(errorElement as any).focus();
}, 250); // Focus only after 250ms when scroll animation if finished
}
}
}, 100); // Ensure everything is rendered to make sure animation will be smooth
});
return null;
};
export const FormikGotoErrorOnSubmitEffect = connect(FormikGotoErrorOnSubmitEffectFn);
// Call cb ONLY once if count is > 0 and ONLY if count is incremented next time.
export function useEffectWhenCountIncremented(count: any, cb: (count: number) => void): void {
const [lastCount, setLastCount] = React.useState(0);
React.useMemo(
() => {
if (typeof count === 'number' && count > lastCount) {
cb(count);
setLastCount(count);
}
},
[count],
);
}
@osv
Copy link
Author

osv commented Feb 2, 2019

Usage

<Formik>
        {({ dirty, isSubmitting, errors }) => {
          return (
            <Form>
              <Field />
              <Field />
              <FormikGotoErrorOnSubmitEffect />
            </Form>
          );
        }}
 </Formik>

discussion thread: jaredpalmer/formik#146 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment