Search code examples
reactjscheckboxscrollformikionic7

How do I scroll to the first error when all my checkbox inputs have the same name?


I'm using React 18, Ionic 7, and Formik 2.4.3. I have a component that I use when I want to scroll to the top the location of an error

export const getFieldErrorNames = (formikErrors) => {
  const transformObjectToDotNotation = (obj, prefix = '', result = []) => {
    Object.keys(obj).forEach((key) => {
      const value = obj[key]
      if (!value) return

      const nextKey = prefix ? `${prefix}.${key}` : key
      if (typeof value === 'object') {
        transformObjectToDotNotation(value, nextKey, result)
      } else {
        result.push(nextKey)
      }
    })

    return result
  }

  return transformObjectToDotNotation(formikErrors)
}

const ScrollToFieldError = ({ submitCount, errors }) => {
  useEffect(() => {
    const fieldErrorNames = getFieldErrorNames(errors)
    if (fieldErrorNames.length <= 0) return

    const elements = document.getElementsByName(fieldErrorNames[0])
    const element = elements[0]
    if (!element) return

    // Scroll to first known error into view
    element.scrollIntoView({ behavior: 'smooth', block: 'center' })
  }, [submitCount])

  return null
}

export default ScrollToFieldError

In my Formik form, I use the component like so

  const validationSchema = Yup.object().shape({
    selectedIds: Yup.array().min(1, 'Select at least one item').required('Select at least one item')
  })
    ...

    <Formik
      enableReinitialize={true}
      initialValues={{ selectedIds }}
      validationSchema={validationSchema}
      onSubmit={(values) => {
        handleSaveAndContinue(values.selectedIds)
      }}
    >
      {(formikProps) => (
        <form onSubmit={formikProps.handleSubmit}>
          <ScrollToFirstError submitCount={formikProps.submitCount} errors={formikProps.errors} />
          {formikProps.touched.selectedIds && formikProps.errors.selectedIds ? (
            <div>{formikProps.errors.selectedIds}</div>
          ) : null}
          <IonList>
            ...
            {items.map((item) => (
              <IonItem key={item.id}>
                <IonCheckbox
                  name='selectedIds'
                  value={item.id}
                  
                >{`${item.name}`}</IonCheckbox>
              </IonItem>
            ))}
          </IonList>
          <IonButton aria-lael='Save And COntinue' type='submit'>
            Submit
          </IonButton>
        </form>
      )}
    </Formik>

The issue is when I click my "Submit" button without checking anything, although my error message properly appears, the scroll to the top of the page does not occur. I would like to keep my component, ScrollToFirstError, as generic as possible as I use it on other types of forms, so curious if there's a way to adjust my existing form to get the functionality to work. An example with the problem is here -- https://stackblitz.com/edit/an5yjh-c2pz8v?file=src%2Fcomponents%2FScrollToFirstError.tsx,src%2Fcomponents%2FMyForm.tsx,package-lock.json


Solution

  • It seems that the values of formikProps.submitCount and formikProps.errors are updated separately. Check the process here.

    They don't update at the same time. The useEffect of ScrollToFieldError only has the dependency of submitCount and not for errors. So when it detects a change for submit, the error array isn't updated yet. Adding errors in dependency will fix your issue.

    const ScrollToFieldError = ({ submitCount, errors }) => {
      useEffect(() => {
        const fieldErrorNames = getFieldErrorNames(errors);
        if (fieldErrorNames.length <= 0) return;
    
        const elements = document.getElementsByName(fieldErrorNames[0]);
        const element = elements[0];
        if (!element) return;
    
        // Scroll to first known error into view
        element.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }, [submitCount, errors]);
    
      return null;
    };