Skip to content

Instantly share code, notes, and snippets.

@dphrag
Created November 1, 2018 23:41
Show Gist options
  • Save dphrag/4db3b453e02567a0bb52592679554a5b to your computer and use it in GitHub Desktop.
Save dphrag/4db3b453e02567a0bb52592679554a5b to your computer and use it in GitHub Desktop.
Formik Scroll To First Invalid Element W/O Refs
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);
@fabb
Copy link

fabb commented Jan 28, 2019

this only scrolls to the first field that fails validation, if the keys in errors match the order of fields in the form

@MontoyaAndres
Copy link

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.

@omarkhatibco
Copy link

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]}"]`;

@Steffi3rd
Copy link

Thanks a lot @dphrag!!

and thanks @omarkhatibco, it works better with your suggestions.

@rscotten
Copy link

rscotten commented Jun 9, 2019

Thank you!!! Works great!

@tbntdima
Copy link

Thanks mate!

@ulitiy
Copy link

ulitiy commented Sep 30, 2019

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);

@AmirTugi
Copy link

AmirTugi commented Nov 17, 2019

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);

@Patrick-Ullrich
Copy link

Patrick-Ullrich commented Feb 11, 2020

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. 👍

@nodox
Copy link

nodox commented Feb 25, 2020

thank you for the hooks version!!!

@archansel
Copy link

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

@GuppuBoss
Copy link

GuppuBoss commented May 21, 2020

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;
};

@robinvdvleuten
Copy link

robinvdvleuten commented Jun 30, 2020

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

@robinvdvleuten
Copy link

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]}"]`);

@robinvdvleuten
Copy link

robinvdvleuten commented Jul 3, 2020

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/).

@mndewitt
Copy link

mndewitt commented Jul 30, 2020

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]}"]`

...

@nelsonorduzGL
Copy link

nelsonorduzGL commented Nov 13, 2020

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?

@ArmenBangits
Copy link

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]}"];

@Saeed-ul-haq
Copy link

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..

@dipeshhkc
Copy link

dipeshhkc commented Jun 10, 2021

(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]);

@talkohavy
Copy link

talkohavy commented Aug 18, 2021

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;

@KristianLake
Copy link

This is great :) (using the react hooks version), I don't think it works with react-select elements though

@S-coder-lx
Copy link

All good but not working with react-select

@dayoolacodes
Copy link

anybody able to fix for react-select?

@rita-scaletech
Copy link

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 .

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