【问题标题】:React - check if element is visible in DOMReact - 检查元素在 DOM 中是否可见
【发布时间】:2017-08-04 20:02:58
【问题描述】:

我正在构建一个表单 - 用户在进入下一个屏幕之前需要回答的一系列问题(单选按钮)。对于字段验证,我使用 yup(npm 包)和 redux 作为状态管理。

对于一个特定的场景/组合,会显示一个新屏幕 (div),要求用户在继续之前进行确认(复选框)。我只想在显示时应用此复选框的验证。

如何使用 React 检查元素 (div) 是否显示在 DOM 中?

我想这样做的方法是将变量“isScreenVisible”设置为 false,如果满足条件,我会将状态更改为“true”。

我正在检查并在 _renderScreen() 中将 'isScreenVisible' 设置为 true 或 false,但由于某种原因,它会进入无限循环。

我的代码:

class Component extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      formisValid: true,
      errors: {},
      isScreenVisible: false
    }

    this.FormValidator = new Validate();
    this.FormValidator.setValidationSchema(this.getValidationSchema());
  }

  areThereErrors(errors) {
    var key, er = false;
    for(key in errors) {
      if(errors[key]) {er = true}
    }
    return er;
  }

  getValidationSchema() {
    return yup.object().shape({
      TravelInsurance: yup.string().min(1).required("Please select an option"),
      MobilePhoneInsurance: yup.string().min(1).required("Please select an option"),
      Confirmation: yup.string().min(1).required("Please confirm"),
    });
  }

  //values of form fields
  getValidationObject() {
    let openConfirmation = (this.props.store.Confirmation === true)? 'confirmed': ''

    return {
      TravelInsurance: this.props.store.TravelInsurance,
      MobilePhoneInsurance: this.props.store.MobilePhoneInsurance,
      Confirmation: openConfirmation,
    }
  }

  setSubmitErrors(errors) {
    this.setState({errors: errors});
  }

  submitForm() {
    var isErrored, prom, scope = this, obj = this.getValidationObject();
    prom = this.FormValidator.validateSubmit(obj);

    prom.then((errors) => {
      isErrored = this.FormValidator.isFormErrored();

      scope.setState({errors: errors}, () => {
        if (isErrored) {
        } else {
          this.context.router.push('/Confirm');
        }
      });
    });
  }

  saveData(e) {
    let data = {}
    data[e.target.name] = e.target.value

    this.props.addData(data)

    this.props.addData({
      Confirmation: e.target.checked
    })
  }

  _renderScreen = () => {
    const {
      Confirmation
    } = this.props.store

    if(typeof(this.props.store.TravelInsurance) !== 'undefined' && typeof(this.props.store.MobilePhoneInsurance) !== 'undefined') &&
    ((this.props.store.TravelInsurance === 'Yes' && this.props.store.MobilePhoneInsurance === 'No') ||
    (this.props.store.TravelInsurance === 'No' && this.props.store.MobilePhoneInsurance === 'Yes')){

        this.setState({
            isScreenVisible: true
        })

          return(
            <div>
                <p>Please confirm that you want to proceed</p>

                  <CheckboxField
                    id="Confirmation"
                    name="Confirmation"
                    value={Confirmation}
                    validationMessage={this.state.errors.Confirmation}
                    label="I confirm that I would like to continue"
                    defaultChecked={!!Confirmation}
                    onClick={(e)=> {this.saveData(e)} }
                  />
                </FormLabel>
            </div>
          )
      }
      else{
        this.setState({
            isScreenVisible: false
        })
      }
  }

  render(){
    const {
      TravelInsurance,
      MobilePhoneInsurance
    } = this.props.store

    return (
      <div>           
          <RadioButtonGroup
            id="TravelInsurance"
            name="TravelInsurance"
            checked={TravelInsurance}
            onClick={this.saveData.bind(this)}
            options={{
              'Yes': 'Yes',
              'No': 'No'
            }}
            validationMessage={(this.state.errors.TravelInsurance) ? this.state.errors.TravelInsurance : null }
          />

        <RadioButtonGroup
          id="MobilePhoneInsurance"
          name="MobilePhoneInsurance"
          checked={MobilePhoneInsurance}
          onClick={this.saveData.bind(this)}
          options={{
            'Yes': 'Yes',
            'No': 'No'
          }}
          validationMessage={(this.state.errors.MobilePhoneInsurance) ? this.state.errors.MobilePhoneInsurance : null }
        />

        this._renderScreen()

        <ButtonRow
            primaryProps={{
                children: 'Continue',
                onClick: e=>{
                this.submitForm();
            }
        }}
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    store: state.Insurance,
  }
}

const Insurance = connect(mapStateToProps,{addData})(Component)

export default Insurance

【问题讨论】:

    标签: javascript reactjs


    【解决方案1】:

    这是一个利用 IntersectionObserver API 的可重用钩子。

    钩子

    export default function useOnScreen(ref) {
    
      const [isIntersecting, setIntersecting] = useState(false)
    
      const observer = new IntersectionObserver(
        ([entry]) => setIntersecting(entry.isIntersecting)
      )
    
      useEffect(() => {
        observer.observe(ref.current)
        // Remove the observer as soon as the component is unmounted
        return () => { observer.disconnect() }
      }, [])
    
      return isIntersecting
    }
    

    用法

    const DummyComponent = () => {
      
      const ref = useRef()
      const isVisible = useOnScreen(ref)
      
      return <div ref={ref}>{isVisible && `Yep, I'm on screen`}</div>
    }
    

    【讨论】:

    • 我猜应该使用 useMemo 来存储交叉点观察者,以免在渲染时重新创建它?!
    • IntersectionObserver 不适用于 Safari 10
    • @Creaforge 你能用备忘录更新答案吗?
    • 这不是在每个渲染上创建一个新的 IntersectionObserver 吗?我相信stackoverflow.com/a/67826055/1727807 中的类似实现会避免这种情况。
    • 在这个钩子中使用 useMemo 相当简单:const observer = useMemo(() =&gt; new IntersectionObserver(…), [ref, rootMargin])。这是一个独立的话题,何时使用 useMemo 来防止额外处理的阈值实际上取决于您的实现
    【解决方案2】:

    您可以将 ref 附加到要检查它是否在视口上的元素上,然后具有以下内容:

      /**
       * Check if an element is in viewport
       *
       * @param {number} [offset]
       * @returns {boolean}
       */
      isInViewport(offset = 0) {
        if (!this.yourElement) return false;
        const top = this.yourElement.getBoundingClientRect().top;
        return (top + offset) >= 0 && (top - offset) <= window.innerHeight;
      }
    
    
      render(){
    
         return(<div ref={(el) => this.yourElement = el}> ... </div>)
    
      }
    

    您可以附加诸如onScroll 之类的侦听器并检查元素何时会出现在视口上。

    您也可以使用Intersection Observer API with a polyfilHoC component that does the job

    【讨论】:

    • 你的代码是怎么工作的,稍微解释一下就行了
    • 何时调用isInViewport()
    • 这是我第一次看到在没有正确代码解释的情况下获得 40+ 票的答案。
    • 为了子孙后代,似乎isInViewport() 会在你的onScroll 监听器中被调用,作者确实提到了这一点,但含糊其辞。
    【解决方案3】:

    根据 Avraam 的回答,我编写了一个兼容 Typescript 的小钩子来满足实际的 React 代码约定。

    import { useRef, useEffect, useState } from "react";
    import throttle from "lodash.throttle";
    
    /**
     * Check if an element is in viewport
    
     * @param {number} offset - Number of pixels up to the observable element from the top
     * @param {number} throttleMilliseconds - Throttle observable listener, in ms
     */
    export default function useVisibility<Element extends HTMLElement>(
      offset = 0,
      throttleMilliseconds = 100
    ): [Boolean, React.RefObject<Element>] {
      const [isVisible, setIsVisible] = useState(false);
      const currentElement = useRef<Element>();
    
      const onScroll = throttle(() => {
        if (!currentElement.current) {
          setIsVisible(false);
          return;
        }
        const top = currentElement.current.getBoundingClientRect().top;
        setIsVisible(top + offset >= 0 && top - offset <= window.innerHeight);
      }, throttleMilliseconds);
    
      useEffect(() => {
        document.addEventListener('scroll', onScroll, true);
        return () => document.removeEventListener('scroll', onScroll, true);
      });
    
      return [isVisible, currentElement];
    }
    
    

    使用示例:

    const Example: FC = () => {
      const [ isVisible, currentElement ] = useVisibility<HTMLDivElement>(100);
    
      return <Spinner ref={currentElement} isVisible={isVisible} />;
    };
    
    

    您可以找到example on Codesandbox。 希望对您有所帮助!

    【讨论】:

    • 你好,最好在代码沙箱或您选择的任何一个中提供这个。请问?
    • 是的,添加了 Codesandbox 的链接。
    • 关于如何使其在 div 中工作的任何见解,例如,将包含项目的 div 及其只是界面的一小部分,这是固定的。
    • createRef 不会在重新渲染时丢失吗?也许改用useRef
    • 将默认设置为true,因此默认隐藏,否则需要用户输入才能检查钩子。
    【解决方案4】:

    我也遇到过同样的问题,看起来,我在纯 react jsx 中找到了相当不错的解决方案,无需安装任何库。

    import React, {Component} from "react";
        
        class OurReactComponent extends Component {
    
        //attach our function to document event listener on scrolling whole doc
        componentDidMount() {
            document.addEventListener("scroll", this.isInViewport);
        }
    
        //do not forget to remove it after destroyed
        componentWillUnmount() {
            document.removeEventListener("scroll", this.isInViewport);
        }
    
        //our function which is called anytime document is scrolling (on scrolling)
        isInViewport = () => {
            //get how much pixels left to scrolling our ReactElement
            const top = this.viewElement.getBoundingClientRect().top;
    
            //here we check if element top reference is on the top of viewport
            /*
            * If the value is positive then top of element is below the top of viewport
            * If the value is zero then top of element is on the top of viewport
            * If the value is negative then top of element is above the top of viewport
            * */
            if(top <= 0){
                console.log("Element is in view or above the viewport");
            }else{
                console.log("Element is outside view");
            }
        };
    
        render() {
            // set reference to our scrolling element
            let setRef = (el) => {
                this.viewElement = el;
            };
            return (
                // add setting function to ref attribute the element which we want to check
                <section ref={setRef}>
                    {/*some code*/}
                </section>
            );
        }
    }
    
    export default OurReactComponent;
    

    我试图弄清楚如果元素在视口中,如何为元素设置动画。

    Here is work project on CodeSandbox.

    【讨论】:

      【解决方案5】:

      @Alex Gusev 回答没有 lodash 并使用 useRef

      import { MutableRefObject, useEffect, useRef, useState } from 'react'
      
      /**
       * Check if an element is in viewport
       * @param {number} offset - Number of pixels up to the observable element from the top
       */
      export default function useVisibility<T>(
        offset = 0,
      ): [boolean, MutableRefObject<T>] {
        const [isVisible, setIsVisible] = useState(false)
        const currentElement = useRef(null)
      
        const onScroll = () => {
          if (!currentElement.current) {
            setIsVisible(false)
            return
          }
          const top = currentElement.current.getBoundingClientRect().top
          setIsVisible(top + offset >= 0 && top - offset <= window.innerHeight)
        }
      
        useEffect(() => {
          document.addEventListener('scroll', onScroll, true)
          return () => document.removeEventListener('scroll', onScroll, true)
        })
      
        return [isVisible, currentElement]
      }
      

      用法示例:

       const [beforeCheckoutSubmitShown, beforeCheckoutSubmitRef] = useVisibility<HTMLDivElement>()
      
       return (
           <div ref={beforeCheckoutSubmitRef} />
      

      【讨论】:

        【解决方案6】:

        根据@Alex Gusev 的帖子回答

        React hook 通过一些修复检查元素是否可见,并基于 rxjs 库。

        import React, { useEffect, createRef, useState } from 'react';
        import { Subject, Subscription } from 'rxjs';
        import { debounceTime, throttleTime } from 'rxjs/operators';
        
        /**
         * Check if an element is in viewport
         * @param {number} offset - Number of pixels up to the observable element from the top
         * @param {number} throttleMilliseconds - Throttle observable listener, in ms
         * @param {boolean} triggerOnce - Trigger renderer only once when element become visible
         */
        export default function useVisibleOnScreen<Element extends HTMLElement>(
          offset = 0,
          throttleMilliseconds = 1000,
          triggerOnce = false,
          scrollElementId = ''
        ): [boolean, React.RefObject<Element>] {
          const [isVisible, setIsVisible] = useState(false);
          const currentElement = createRef<Element>();
        
          useEffect(() => {
            let subscription: Subscription | null = null;
            let onScrollHandler: (() => void) | null = null;
            const scrollElement = scrollElementId
              ? document.getElementById(scrollElementId)
              : window;
            const ref = currentElement.current;
            if (ref && scrollElement) {
              const subject = new Subject();
              subscription = subject
                .pipe(throttleTime(throttleMilliseconds))
                .subscribe(() => {
                  if (!ref) {
                    if (!triggerOnce) {
                      setIsVisible(false);
                    }
                    return;
                  }
        
                  const top = ref.getBoundingClientRect().top;
                  const visible =
                    top + offset >= 0 && top - offset <= window.innerHeight;
                  if (triggerOnce) {
                    if (visible) {
                      setIsVisible(visible);
                    }
                  } else {
                    setIsVisible(visible);
                  }
                });
              onScrollHandler = () => {
                subject.next();
              };
              if (scrollElement) {
                scrollElement.addEventListener('scroll', onScrollHandler, false);
              }
              // Check when just loaded:
              onScrollHandler();
            } else {
              console.log('Ref or scroll element cannot be found.');
            }
        
            return () => {
              if (onScrollHandler && scrollElement) {
                scrollElement.removeEventListener('scroll', onScrollHandler, false);
              }
              if (subscription) {
                subscription.unsubscribe();
              }
            };
            // eslint-disable-next-line react-hooks/exhaustive-deps
          }, [offset, throttleMilliseconds, triggerOnce, scrollElementId]);
        
          return [isVisible, currentElement];
        }
        

        【讨论】:

          猜你喜欢
          • 2013-11-09
          • 2022-01-07
          • 2017-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-04-21
          • 1970-01-01
          • 2018-07-09
          • 2017-05-23
          相关资源
          最近更新 更多