首页 文章

检测React组件外部的单击

提问于
浏览
197

我正在寻找一种方法来检测点击事件是否发生在组件之外,如article中所述 . jQuery nearest()用于查看click事件中的目标是否具有dom元素作为其父元素之一 . 如果匹配,则click事件属于其中一个子项,因此不被视为在组件之外 .

所以在我的组件中,我想将一个单击处理程序附加到窗口 . 当处理程序触发时,我需要将目标与我的组件的dom子项进行比较 .

click事件包含像“path”这样的属性,它似乎保存了事件所经过的dom路径 . 我不确定要比较什么或如何最好地遍历它,我认为有人必须已经把它放在一个聪明的实用功能中......不是吗?

20 回答

  • 304

    这里没有其他答案对我有用 . 我试图隐藏模糊的弹出窗口,但由于内容是绝对定位的,onBlur甚至在点击内部内容时也开始了 .

    这是一种对我有用的方法:

    // Inside the component:
    onBlur(event) {
        // currentTarget refers to this component.
        // relatedTarget refers to the element where the user clicked (or focused) which
        // triggered this event.
        // So in effect, this condition checks if the user clicked outside the component.
        if (!event.currentTarget.contains(event.relatedTarget)) {
            // do your thing.
        }
    },
    

    希望这可以帮助 .

  • 2
    componentWillMount(){
    
      document.addEventListener('mousedown', this.handleClickOutside)
    }
    
    handleClickOutside(event) {
    
      if(event.path[0].id !== 'your-button'){
         this.setState({showWhatever: false})
      }
    }
    

    事件 path[0] 是单击的最后一项

  • 76

    以下是最适合我的解决方案,无需将事件附加到容器:

    某些HTML元素可以具有所谓的“ focus ”,例如输入元素 . 当这些元素失去焦点时,它们也会响应模糊事件 .

    要为任何元素提供焦点容量,只需确保其tabindex属性设置为-1以外的任何值 . 在常规HTML中,可以通过设置tabindex属性,但在React中你必须使用tabIndex(注意大写I) .

    您也可以通过JavaScript进行 element.setAttribute('tabindex',0)

    这就是我用它来制作自定义的DropDown菜单 .

    var DropDownMenu = React.createClass({
        getInitialState: function(){
            return {
                expanded: false
            }
        },
        expand: function(){
            this.setState({expanded: true});
        },
        collapse: function(){
            this.setState({expanded: false});
        },
        render: function(){
            if(this.state.expanded){
                var dropdown = ...; //the dropdown content
            } else {
                var dropdown = undefined;
            }
    
            return (
                <div className="dropDownMenu" tabIndex="0" onBlur={ this.collapse } >
                    <div className="currentValue" onClick={this.expand}>
                        {this.props.displayValue}
                    </div>
                    {dropdown}
                </div>
            );
        }
    });
    
  • 0

    我在下面的文章中找到了这个:

    render(){return(}> Toggle Popover {this.state.popupVisible &&(我是一个popover!)}); }}

    这是一篇关于这个问题的好文章: "Handle clicks outside of React components" https://larsgraubner.com/handle-outside-clicks-react/

  • 1

    您可以在主体上安装双击处理程序,在此元素上安装另一个处理程序 . 在此元素的处理程序中,只返回false以防止事件传播 . 因此,当双击发生时,如果它在元素上,它将被捕获并且不会传播到正文上的处理程序 . 否则它将被身体上的处理程序捕获 .

    更新:如果你真的不想阻止事件传播,你只需要使用最近的来检查你的元素或他的孩子之间是否发生了点击:

    <html>
    <head>
    <script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
    <script>
    $(document).on('click', function(event) {
        if (!$(event.target).closest('#div3').length) {
        alert("outside");
        }
    });
    </script>
    </head>
    <body>
        <div style="background-color:blue;width:100px;height:100px;" id="div1"></div>
        <div style="background-color:red;width:100px;height:100px;" id="div2"></div>
        <div style="background-color:green;width:100px;height:100px;" id="div3"></div>
        <div style="background-color:yellow;width:100px;height:100px;" id="div4"></div>
        <div style="background-color:grey;width:100px;height:100px;" id="div5"></div>
    </body>
    </html>
    

    更新:没有jQuery:

    <html>
    <head>
    <script>
    function findClosest (element, fn) {
      if (!element) return undefined;
      return fn(element) ? element : findClosest(element.parentElement, fn);
    }
    document.addEventListener("click", function(event) {
        var target = findClosest(event.target, function(el) {
            return el.id == 'div3'
        });
        if (!target) {
            alert("outside");
        }
    }, false);
    </script>
    </head>
    <body>
        <div style="background-color:blue;width:100px;height:100px;" id="div1"></div>
        <div style="background-color:red;width:100px;height:100px;" id="div2"></div>
        <div style="background-color:green;width:100px;height:100px;" id="div3">
            <div style="background-color:pink;width:50px;height:50px;" id="div6"></div>
        </div>
        <div style="background-color:yellow;width:100px;height:100px;" id="div4"></div>
        <div style="background-color:grey;width:100px;height:100px;" id="div5"></div>
    </body>
    </html>
    
  • 110

    要扩展Ben Bud所接受的答案,如果你使用样式组件,那么传递refs会给你一个错误,如"this.wrapperRef.contains is not a function" .

    在注释中建议的修复,用div包装样式组件并将ref传递到那里,起作用 . 话虽如此,在他们的docs中他们已经解释了这个的原因以及在样式组件中正确使用ref:

    将ref prop传递给样式化组件将为您提供StyledComponent包装器的实例,但不会为底层DOM节点提供实例 . 这是由于refs如何工作 . 不可能直接在我们的包装器上调用DOM方法,比如focus . 要获取实际的包装DOM节点的引用,请将回调传递给innerRef prop .

    像这样:

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

    然后您可以直接在“handleClickOutside”函数中访问它:

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

    这也适用于“onBlur”方法:

    componentDidMount(){
        this.el.focus()
    }
    blurHandler = () => {
        console.log('clicked outside')
    }
    render(){
        return(
            <StyledDiv
                onBlur={this.blurHandler}
                tabIndex="0"
                innerRef={el => { this.el = el }}
            />
        )
    }
    
  • 21

    以下解决方案使用ES6并遵循绑定的最佳实践以及通过方法设置ref .

    要看到它的实际效果:Code Sandbox demo

    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.setWrapperRef = this.setWrapperRef.bind(this);
        this.handleClickOutside = this.handleClickOutside.bind(this);
      }
    
      componentDidMount() {
        document.addEventListener('mousedown', this.handleClickOutside);
      }
    
      componentWillUnmount() {
        document.removeEventListener('mousedown', this.handleClickOutside);
      }
    
      /**
       * Set the wrapper ref
       */
      setWrapperRef(node) {
        this.wrapperRef = node;
      }
    
      /**
       * Alert if clicked on outside of element
       */
      handleClickOutside(event) {
        if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
          alert('You clicked outside of me!');
        }
      }
    
      render() {
        return <div ref={this.setWrapperRef}>{this.props.children}</div>;
      }
    }
    
    OutsideAlerter.propTypes = {
      children: PropTypes.element.isRequired,
    };
    
  • 3

    在这里尝试了很多方法之后,我决定使用github.com/Pomax/react-onclickoutside因为它有多完整 .

    我通过npm安装了模块并将其导入到我的组件中:

    import onClickOutside from 'react-onclickoutside'
    

    然后,在我的组件类中,我定义了 handleClickOutside 方法:

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

    导出我的组件时,我将其包装在 onClickOutside() 中:

    export default onClickOutside(NameOfComponent)
    

    而已 .

  • 37

    我用this module(我与作者没有关联)

    npm install react-onclickout --save
    
    const ClickOutHandler = require('react-onclickout');
     
    class ExampleComponent extends React.Component {
     
      onClickOut(e) {
        if (hasClass(e.target, 'ignore-me')) return;
        alert('user clicked outside of the component!');
      }
     
      render() {
        return (
          <ClickOutHandler onClickOut={this.onClickOut}>
            <div>Click outside of me!</div>
          </ClickOutHandler>
        );
      }
    }
    

    它做得很好 .

  • 1

    我这样做的部分原因是遵循this并遵循React官方文档处理需要反应的refs ^ 16.3 . 在尝试了其他一些建议后,这是唯一有用的东西......

    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 => {
        if (this.inputRef.current === e.target) {
          return;
        }
        this.handleclickOutside();
      };
    handleClickOutside(){
    ...***code to handle what to do when clicked outside***...
    }
    render(){
    return(
    <div>
    ...***code for what's outside***...
    <span ref={this.inputRef}>
    ...***code for what's "inside"***...
    </span>
    ...***code for what's outside***
    )}}
    
  • 8

    我被困在同一个问题上 . 我在这里参加聚会有点晚了,但对我来说这是一个非常好的解决方案 . 希望它对别人有帮助 . 你需要从 react-dom 导入 findDOMNode

    import ReactDOM from 'react-dom';
    // ... ✂
    
    componentDidMount() {
        document.addEventListener('click', this.handleClickOutside, true);
    }
    
    componentWillUnmount() {
        document.removeEventListener('click', this.handleClickOutside, true);
    }
    
    handleClickOutside = event => {
        const domNode = ReactDOM.findDOMNode(this);
    
        if (!domNode || !domNode.contains(event.target)) {
            this.setState({
                visible : false
            });
        }
    }
    
  • 1

    对于那些需要绝对定位的人来说,我选择的一个简单选项就是添加一个包装器组件,该组件的样式覆盖了整个页面的透明背景 . 然后,您可以在此元素上添加onClick以关闭内部组件 .

    <div style={{
            position: 'fixed',
            top: '0', right: '0', bottom: '0', left: '0',
            zIndex: '1000',
          }} onClick={() => handleOutsideClick()} >
        <Content style={{position: 'absolute'}}/>
    </div>
    

    现在,如果您在内容上添加单击处理程序,该事件也将传播到上部div和因此触发handlerOutsideClick . 如果这不是您想要的行为,只需停止处理程序上的事件进程 .

    <Content style={{position: 'absolute'}} onClick={e => {
                                              e.stopPropagation();
                                              desiredFunctionCall();
                                            }}/>
    

    `

  • 0

    感谢Ben Alpert在discuss.reactjs.org找到了解决方案 . 建议的方法将处理程序附加到文档,但结果证明是有问题的 . 单击树中的某个组件会导致重新呈现,从而删除更新时单击的元素 . 因为React的rerender在调用文档正文处理程序之前发生,所以该元素未被检测为"inside"树 .

    解决方案是在应用程序根元素上添加处理程序 .

    主要:

    window.__myapp_container = document.getElementById('app')
    React.render(<App/>, window.__myapp_container)
    

    零件:

    import { Component, PropTypes } from 'react';
    import ReactDOM from 'react-dom';
    
    export default class ClickListener extends Component {
    
      static propTypes = {
        children: PropTypes.node.isRequired,
        onClickOutside: PropTypes.func.isRequired
      }
    
      componentDidMount () {
        window.__myapp_container.addEventListener('click', this.handleDocumentClick)
      }
    
      componentWillUnmount () {
        window.__myapp_container.removeEventListener('click', this.handleDocumentClick)
      }
    
      /* using fat arrow to bind to instance */
      handleDocumentClick = (evt) => {
        const area = ReactDOM.findDOMNode(this.refs.area);
    
        if (!area.contains(evt.target)) {
          this.props.onClickOutside(evt)
        }
      }
    
      render () {
        return (
          <div ref='area'>
           {this.props.children}
          </div>
        )
      }
    }
    
  • 37

    这是我的方法(演示 - https://jsfiddle.net/agymay93/4/):

    我创建了一个名为 WatchClickOutside 的特殊组件,它可以像(我假设 JSX 语法)一样使用:

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

    这是 WatchClickOutside 组件的代码:

    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>
        );
      }
    }
    
  • 1

    战略的一个例子

    我喜欢提供的解决方案,通过在组件周围创建包装器来执行相同的操作 .

    因为这更像是一种我认为是战略的行为,并提出了以下建议 .

    我是React的新手,我需要一些帮助才能在用例中保存一些样板

    请检讨并告诉我你的想法 .

    ClickOutsideBehavior

    import ReactDOM from 'react-dom';
    
    export default class ClickOutsideBehavior {
    
      constructor({component, appContainer, onClickOutside}) {
    
        // Can I extend the passed component's lifecycle events from here?
        this.component = component;
        this.appContainer = appContainer;
        this.onClickOutside = onClickOutside;
      }
    
      enable() {
    
        this.appContainer.addEventListener('click', this.handleDocumentClick);
      }
    
      disable() {
    
        this.appContainer.removeEventListener('click', this.handleDocumentClick);
      }
    
      handleDocumentClick = (event) => {
    
        const area = ReactDOM.findDOMNode(this.component);
    
        if (!area.contains(event.target)) {
            this.onClickOutside(event)
        }
      }
    }
    

    样本用法

    import React, {Component} from 'react';
    import {APP_CONTAINER} from '../const';
    import ClickOutsideBehavior from '../ClickOutsideBehavior';
    
    export default class AddCardControl extends Component {
    
      constructor() {
        super();
    
        this.state = {
          toggledOn: false,
          text: ''
        };
    
        this.clickOutsideStrategy = new ClickOutsideBehavior({
          component: this,
          appContainer: APP_CONTAINER,
          onClickOutside: () => this.toggleState(false)
        });
      }
    
      componentDidMount () {
    
        this.setState({toggledOn: !!this.props.toggledOn});
        this.clickOutsideStrategy.enable();
      }
    
      componentWillUnmount () {
        this.clickOutsideStrategy.disable();
      }
    
      toggleState(isOn) {
    
        this.setState({toggledOn: isOn});
      }
    
      render() {...}
    }
    

    注意事项

    我想过存储传递的 component 生命周期钩子并使用simillar方法覆盖它们:

    const baseDidMount = component.componentDidMount;
    
    component.componentDidMount = () => {
      this.enable();
      baseDidMount.call(component)
    }
    

    component 是传递给 ClickOutsideBehavior 构造函数的组件 .
    这将删除此行为的用户的启用/禁用样板,但它看起来不是很好

  • 2

    我对所有其他答案的最大关注是必须从root / parent中过滤点击事件 . 我发现最简单的方法是简单地设置一个兄弟元素,其位置为:fixed,在下拉列表后面有一个z-index 1,并处理同一组件内固定元素上的click事件 . 将所有内容集中到给定组件 .

    示例代码

    #HTML
    <div className="parent">
      <div className={`dropdown ${this.state.open ? open : ''}`}>
        ...content
      </div>
      <div className="outer-handler" onClick={() => this.setState({open: false})}>
      </div>
    </div>
    
    #SASS
    .dropdown {
      display: none;
      position: absolute;
      top: 0px;
      left: 0px;
      z-index: 100;
      &.open {
        display: block;
      }
    }
    .outer-handler {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        opacity: 0;
        z-index: 99;
        display: none;
        &.open {
          display: block;
        }
    }
    
  • -6

    这已经有很多答案,但它们没有解决 e.stopPropagation() 并阻止单击要关闭的元素之外的反应链接 .

    由于React让它_1138441_能够使用文档作为事件监听器的基础 . 在此之前你需要 e.stopPropagation() ,因为React使用文档本身 . 如果您使用例如 document.querySelector('body') . 您可以阻止来自React链接的点击 . 以下是我如何实现外部点击和关闭的示例 .
    这使用 ES6React 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

    onClick 处理程序添加到顶级容器,并在用户单击时增加状态值 . 将该值传递给相关组件,只要值发生变化,您就可以开始工作 .

    在这种情况下,只要 clickCount 值发生变化,我们就会调用 this.closeDropdown() .

    incrementClickCount 方法在 .app 容器内触发,但不在 .dropdown 内触发,因为我们使用 event.stopPropagation() 来防止事件冒泡 .

    您的代码可能最终看起来像这样:

    class App extends Component {
        constructor(props) {
            super(props);
            this.state = {
                clickCount: 0
            };
        }
        incrementClickCount = () => {
            this.setState({
                clickCount: this.state.clickCount + 1
            });
        }
        render() {
            return (
                <div className="app" onClick={this.incrementClickCount}>
                    <Dropdown clickCount={this.state.clickCount}/>
                </div>
            );
        }
    }
    class Dropdown extends Component {
        constructor(props) {
            super(props);
            this.state = {
                open: false
            };
        }
        componentDidUpdate(prevProps) {
            if (this.props.clickCount !== prevProps.clickCount) {
                this.closeDropdown();
            }
        }
        toggleDropdown = event => {
            event.stopPropagation();
            return (this.state.open) ? this.closeDropdown() : this.openDropdown();
        }
        render() {
            return (
                <div className="dropdown" onClick={this.toggleDropdown}>
                    ...
                </div>
            );
        }
    }
    
  • 0

    这是我的解决方案 React 16.3

    CodeSandbox

    import React, { Component } from "react";
    
    class SampleComponent extends Component {
      state = {
        clickedOutside: false
      };
    
      componentDidMount() {
        document.addEventListener("mousedown", this.handleClickOutside);
      }
    
      componentWillUnmount() {
        document.removeEventListener("mousedown", this.handleClickOutside);
      }
    
      myRef = React.createRef();
    
      handleClickOutside = e => {
        if (!this.myRef.current.contains(e.target)) {
          this.setState({ clickedOutside: true });
        }
      };
    
      handleClickInside = () => this.setState({ clickedOutside: false });
    
      render() {
        return (
          <button ref={this.myRef} onClick={this.handleClickInside}>
            {this.state.clickedOutside ? "Bye!" : "Hello!"}
          </button>
        );
      }
    }
    
    export default SampleComponent;
    
  • 2

    要使 'focus' 解决方案适用于使用事件侦听器的下拉列表,您可以使用 onMouseDown 事件而不是 onClick 添加它们 . 这样事件就会触发,之后弹出窗口就会关闭:

    <TogglePopupButton
                        onClick = { this.toggleDropup }
                        tabIndex = '0'
                        onBlur = { this.closeDropup }
                    />
                    { this.state.isOpenedDropup &&
                    <ul className = { dropupList }>
                        { this.props.listItems.map((item, i) => (
                            <li
                                key = { i }
                                onMouseDown = { item.eventHandler }
                            >
                                { item.itemName}
                            </li>
                        ))}
                    </ul>
                    }
    

相关问题