【问题标题】:Scroll to first invalid field with formik and useRefs react使用 formik 滚动到第一个无效字段并 useRefs 做出反应
【发布时间】:2021-01-26 10:28:52
【问题描述】:

我正在尝试修改下面的代码以使用 react useRef 而不是使用 document.querySelector(selector) as HTMLElement;,因为它不是反应的最佳实践。我正在尝试实现在 Formik 表单上滚动到第一个错误的功能,此代码确实有效,但我如何使用 React useRef 来做到这一点?

代码如下:

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>

【问题讨论】:

    标签: reactjs formik


    【解决方案1】:

    而不是做-

    errorElement.focus();
    

    这样做 -

    errorElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
    errorElement.focus({ preventScroll: true });
    

    【讨论】:

      【解决方案2】:

      我的解决方案是将其构建到我的自定义表单字段中。我使用 MaterialUI TextFields 但你当然可以不用。相关代码:

      import { TextField as MuiTextField } from '@material-ui/core'
      
      export const TextField = ({ name, ...rest }) => {
        const [field, meta] = useField({ name, type: 'text' })
        const formikBag = useFormikContext()
        const fieldRef = useRef<HTMLDivElement>(null)
      
        useEffect(() => {
          const firstError = Object.keys(formikBag.errors)[0]
          if (formikBag.isSubmitting && firstError === name) {
            fieldRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
          }
        }, [meta.error, formikBag.isSubmitting, name, formikBag.errors])
      
        return (
          <MuiTextField
            id={field.name}
            ref={fieldRef}
            error={meta.touched && Boolean(meta.error)}
            helperText={meta.touched && meta.error}
            {...rest}
            {...field}
          />
        )
      }
      
      

      您甚至可以将其提取到这样的自定义挂钩中:

      import { useFormikContext } from 'formik'
      import { useRef, useEffect, MutableRefObject } from 'react'
      
      export const useScrollToInvalidField = <T extends HTMLElement>(name: string): MutableRefObject<T | null> => {
        const formikBag = useFormikContext()
        const fieldRef = useRef<T>(null)
      
        useEffect(() => {
          const firstError = Object.keys(formikBag.errors)[0]
          if (formikBag.isSubmitting && firstError === name) {
            fieldRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
          }
        }, [formikBag.isSubmitting, name, formikBag.errors])
        return fieldRef
      }
      

      然后你只需给它正确的字段元素类型(例如HTMLDivElement)并将引用附加到表单字段组件。

      【讨论】:

      • 这很好,但只有在 DOM 中表单输入的顺序与 Formik 验证错误的顺序匹配时才会起作用。有时可能是这样,但并非总是如此。
      • 我在这里发现的另一个限制是 Formik 的 validateOnBlur 必须设置为 true。如果将其设置为 false,则可以触发一种情况,即在提交表单时,以前无效的字段(已被修复为有效但 Formik 验证尚未重新运行)仍会将焦点移至它.
      • 我想我要说的第三点是为了可访问性(我想也是可用性)我们应该将 focus 移动到第一个无效输入,而不仅仅是滚动它进入视野。
      【解决方案3】:

      我正在使用 useFormik 并像这样制作了 useEffect 钩子。

      
        const formik = useFormik({
          initialValues: initialValues,
          validationSchema: validators,
          onSubmit: (values, {setErrors}) => {
            ....
          }
        });
      
        useEffect(() => {
          if (!formik.isSubmitting) return;
          if (Object.keys(formik.errors).length > 0) {
            document.getElementsByName(Object.keys(formik.errors)[0])[0].focus();
          }
        }, [formik]);
      

      并将焦点设置到第一个无效输入字段。 我希望这对其他人有帮助。

      【讨论】:

        【解决方案4】:

        这里已经有一些很好的答案,但我最终选择了一条稍微不同的路线,有些人可能会觉得很有吸引力。

        // in my ScrollToError.tsx file...
        import { useFormikContext } from 'formik';
        import React, { useEffect } from 'react';
        
        export function ScrollToError() {
            const formik = useFormikContext();
            const submitting = formik?.isSubmitting;
        
            useEffect(() => {
                const el = document.querySelector('.Mui-error, [data-error]');
                (el?.parentElement ?? el)?.scrollIntoView();
            }, [submitting]);
            return null;
        }
        
        // later inside my <Formik/> component...
        <ScrollToError/>
        

        这会找到第一个 .Mui-error 元素(通常是相关字段的标签)并关注其父元素,如果没有父元素,则关注其自身。在某些情况下,我添加了一个&lt;Alert severity="error"/&gt; 组件来显示一般错误消息,如果我使用 data-error 属性对其进行标记,也会发现该错误消息。滚动仅在 isSubmitting 更改时发生。

        【讨论】:

          【解决方案5】:

          这是对 4nduril 答案的轻微更新。

          这种变化:

          1. 使用focus 而不是仅使用scrollIntoView(更好的可访问性 - 请参阅https://webaim.org/techniques/formvalidation/)。请注意,除了设置焦点之外,滚动(包括平滑滚动)仍然可用。
          2. 使用usePrevious (https://ahooks.js.org/hooks/use-previous/) 仅将焦点设置到无效字段 提交已从 true 转换为 false。这避免了 Formik 的 validateOnBlur 在 4nduril 的回答中必须设置为 true 的限制。
          3. 确保输入标签始终滚动到视图中,同时输入本身被聚焦。
          export const useFocusInvalidField = <
            T extends HTMLElement,
            U extends HTMLElement,
          >(
            name: string,
            smoothScroll?: boolean,
          ) => {
            const formikContext = useFormikContext();
            // The input/select itself, this is what we will be focusing.
            const fieldRef = useRef<T>(null);
            // The container element that contains both the input/select and its label.
            // This is what we will be scrolling into view. Doing this helps guarantee that
            // the label will be visible. If the submit button is at the bottom of the form and the
            // label is above the invalid input, _just_ focusing the input could leave the
            // label offscreen (above the viewport) which can make it difficult for the user to
            // identify the invalid field.
            const containerElementRef = useRef<U>(null);
          
            const wasSubmitting = usePrevious(formikContext.isSubmitting);
          
            useEffect(() => {
              const firstError = Object.keys(formikContext.errors)[0];
              if (
                wasSubmitting === true &&
                !formikContext.isSubmitting &&
                firstError === name &&
                fieldRef.current !== null
              ) {
                // Use a helper function from https://github.com/oaf-project/oaf-side-effects.
                // This does some browser compatibility and accessibility work for us.
                // eslint-disable-next-line @typescript-eslint/no-floating-promises
                focusAndScrollIntoViewIfRequired(
                  fieldRef.current, // focus the input/select
                  containerElementRef.current ?? fieldRef.current, // scroll the container (and label) into view if set, otherwise scroll to the input
                  smoothScroll, // optionally use smooth scroll. Accessibility note: this will ignore smooth scroll if the user has indicated a preference for reduced motion.
                );
              }
            }, [
              wasSubmitting,
              formikContext.isSubmitting,
              name,
              formikContext.errors,
              smoothScroll,
            ]);
            return { fieldRef, containerElementRef };
          };
          

          【讨论】:

            猜你喜欢
            • 2017-08-02
            • 1970-01-01
            • 1970-01-01
            • 2022-01-20
            • 1970-01-01
            • 2020-07-14
            • 2022-10-18
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多