Detect click outside React component

后端 未结 30 1107
日久生厌
日久生厌 2020-11-22 13:54

I\'m looking for a way to detect if a click event happened outside of a component, as described in this article. jQuery closest() is used to see if the target from a click e

相关标签:
30条回答
  • 2020-11-22 14:11

    Here is my approach (demo - https://jsfiddle.net/agymay93/4/):

    I've created special component called WatchClickOutside and it can be used like (I assume JSX syntax):

    <WatchClickOutside onClickOutside={this.handleClose}>
      <SomeDropdownEtc>
    </WatchClickOutside>
    

    Here is code of WatchClickOutside component:

    import React, { Component } from 'react';
    
    export default class WatchClickOutside extends Component {
      constructor(props) {
        super(props);
        this.handleClick = this.handleClick.bind(this);
      }
    
      componentWillMount() {
        document.body.addEventListener('click', this.handleClick);
      }
    
      componentWillUnmount() {
        // remember to remove all events to avoid memory leaks
        document.body.removeEventListener('click', this.handleClick);
      }
    
      handleClick(event) {
        const {container} = this.refs; // get container that we'll wait to be clicked outside
        const {onClickOutside} = this.props; // get click outside callback
        const {target} = event; // get direct click event target
    
        // if there is no proper callback - no point of checking
        if (typeof onClickOutside !== 'function') {
          return;
        }
    
        // if target is container - container was not clicked outside
        // if container contains clicked target - click was not outside of it
        if (target !== container && !container.contains(target)) {
          onClickOutside(event); // clicked outside - fire callback
        }
      }
    
      render() {
        return (
          <div ref="container">
            {this.props.children}
          </div>
        );
      }
    }
    
    0 讨论(0)
  • 2020-11-22 14:12

    Refs usage in React 16.3+ changed.

    The following solution uses ES6 and follows best practices for binding as well as setting the ref through a method.

    To see it in action:

    • Class Implementation
    • Hooks Implementation

    Class Implementation:

    import React, { Component } from 'react';
    import PropTypes from 'prop-types';
    
    /**
     * Component that alerts if you click outside of it
     */
    export default class OutsideAlerter extends Component {
        constructor(props) {
            super(props);
    
            this.wrapperRef = React.createRef();
            this.setWrapperRef = this.setWrapperRef.bind(this);
            this.handleClickOutside = this.handleClickOutside.bind(this);
        }
    
        componentDidMount() {
            document.addEventListener('mousedown', this.handleClickOutside);
        }
    
        componentWillUnmount() {
            document.removeEventListener('mousedown', this.handleClickOutside);
        }
    
        /**
         * Alert if clicked on outside of element
         */
        handleClickOutside(event) {
            if (this.wrapperRef && !this.wrapperRef.current.contains(event.target)) {
                alert('You clicked outside of me!');
            }
        }
    
        render() {
            return <div ref={this.wrapperRef}>{this.props.children}</div>;
        }
    }
    
    OutsideAlerter.propTypes = {
        children: PropTypes.element.isRequired,
    };
    

    Hooks Implementation:

    import React, { useRef, useEffect } from "react";
    
    /**
     * Hook that alerts clicks outside of the passed ref
     */
    function useOutsideAlerter(ref) {
        useEffect(() => {
            /**
             * Alert if clicked on outside of element
             */
            function handleClickOutside(event) {
                if (ref.current && !ref.current.contains(event.target)) {
                    alert("You clicked outside of me!");
                }
            }
    
            // Bind the event listener
            document.addEventListener("mousedown", handleClickOutside);
            return () => {
                // Unbind the event listener on clean up
                document.removeEventListener("mousedown", handleClickOutside);
            };
        }, [ref]);
    }
    
    /**
     * Component that alerts if you click outside of it
     */
    export default function OutsideAlerter(props) {
        const wrapperRef = useRef(null);
        useOutsideAlerter(wrapperRef);
    
        return <div ref={wrapperRef}>{props.children}</div>;
    }
    
    0 讨论(0)
  • 2020-11-22 14:13

    After trying many methods here, I decided to use github.com/Pomax/react-onclickoutside because of how complete it is.

    I installed the module via npm and imported it into my component:

    import onClickOutside from 'react-onclickoutside'
    

    Then, in my component class I defined the handleClickOutside method:

    handleClickOutside = () => {
      console.log('onClickOutside() method called')
    }
    

    And when exporting my component I wrapped it in onClickOutside():

    export default onClickOutside(NameOfComponent)
    

    That's it.

    0 讨论(0)
  • 2020-11-22 14:14

    This already has many answers but they don't address e.stopPropagation() and preventing clicking on react links outside of the element you wish to close.

    Due to the fact that React has it's own artificial event handler you aren't able to use document as the base for event listeners. You need to e.stopPropagation() before this as React uses document itself. If you use for example document.querySelector('body') instead. You are able to prevent the click from the React link. Following is an example of how I implement click outside and close.
    This uses ES6 and React 16.3.

    import React, { Component } from 'react';
    
    class App extends Component {
      constructor(props) {
        super(props);
    
        this.state = {
          isOpen: false,
        };
    
        this.insideContainer = React.createRef();
      }
    
      componentWillMount() {
        document.querySelector('body').addEventListener("click", this.handleClick, false);
      }
    
      componentWillUnmount() {
        document.querySelector('body').removeEventListener("click", this.handleClick, false);
      }
    
      handleClick(e) {
        /* Check that we've clicked outside of the container and that it is open */
        if (!this.insideContainer.current.contains(e.target) && this.state.isOpen === true) {
          e.preventDefault();
          e.stopPropagation();
          this.setState({
            isOpen: false,
          })
        }
      };
    
      togggleOpenHandler(e) {
        e.preventDefault();
    
        this.setState({
          isOpen: !this.state.isOpen,
        })
      }
    
      render(){
        return(
          <div>
            <span ref={this.insideContainer}>
              <a href="#open-container" onClick={(e) => this.togggleOpenHandler(e)}>Open me</a>
            </span>
            <a href="/" onClick({/* clickHandler */})>
              Will not trigger a click when inside is open.
            </a>
          </div>
        );
      }
    }
    
    export default App;
    
    0 讨论(0)
  • 2020-11-22 14:14

    To extend on the accepted answer made by Ben Bud, if you are using styled-components, passing refs that way will give you an error such as "this.wrapperRef.contains is not a function".

    The suggested fix, in the comments, to wrap the styled component with a div and pass the ref there, works. Having said that, in their docs they already explain the reason for this and the proper use of refs within styled-components:

    Passing a ref prop to a styled component will give you an instance of the StyledComponent wrapper, but not to the underlying DOM node. This is due to how refs work. It's not possible to call DOM methods, like focus, on our wrappers directly. To get a ref to the actual, wrapped DOM node, pass the callback to the innerRef prop instead.

    Like so:

    <StyledDiv innerRef={el => { this.el = el }} />
    

    Then you can access it directly within the "handleClickOutside" function:

    handleClickOutside = e => {
        if (this.el && !this.el.contains(e.target)) {
            console.log('clicked outside')
        }
    }
    

    This also applies for the "onBlur" approach:

    componentDidMount(){
        this.el.focus()
    }
    blurHandler = () => {
        console.log('clicked outside')
    }
    render(){
        return(
            <StyledDiv
                onBlur={this.blurHandler}
                tabIndex="0"
                innerRef={el => { this.el = el }}
            />
        )
    }
    
    0 讨论(0)
  • 2020-11-22 14:14

    I did this partly by following this and by following the React official docs on handling refs which requires react ^16.3. This is the only thing that worked for me after trying some of the other suggestions here...

    class App extends Component {
      constructor(props) {
        super(props);
        this.inputRef = React.createRef();
      }
      componentWillMount() {
        document.addEventListener("mousedown", this.handleClick, false);
      }
      componentWillUnmount() {
        document.removeEventListener("mousedown", this.handleClick, false);
      }
      handleClick = e => {
        /*Validating click is made inside a component*/
        if ( this.inputRef.current === e.target ) {
          return;
        }
        this.handleclickOutside();
      };
      handleClickOutside(){
        /*code to handle what to do when clicked outside*/
      }
      render(){
        return(
          <div>
            <span ref={this.inputRef} />
          </div>
        )
      }
    }
    
    0 讨论(0)
提交回复
热议问题