【问题标题】:How to listen for click events that are outside of a component如何监听组件外部的点击事件
【发布时间】:2014-07-12 08:57:05
【问题描述】:

我想在下拉组件外部发生点击时关闭下拉菜单。

我该怎么做?

【问题讨论】:

    标签: reactjs


    【解决方案1】:

    使用生命周期方法向文档添加和删除事件监听器。

    React.createClass({
        handleClick: function (e) {
            if (this.getDOMNode().contains(e.target)) {
                return;
            }
        },
    
        componentWillMount: function () {
            document.addEventListener('click', this.handleClick, false);
        },
    
        componentWillUnmount: function () {
            document.removeEventListener('click', this.handleClick, false);
        }
    });
    

    查看此组件的第 48-54 行:https://github.com/i-like-robots/react-tube-tracker/blob/91dc0129a1f6077bef57ea4ad9a860be0c600e9d/app/component/tube-tracker.jsx#L48-54

    【讨论】:

    • 这会将一个添加到文档中,但这意味着组件上的任何点击事件也会触发文档事件。这会导致它自己的问题,因为文档侦听器的目标始终是组件末尾的某个低位 div,而不是组件本身。
    • 我不太确定我是否遵循您的评论,但如果您将事件侦听器绑定到文档,您可以过滤任何冒泡的点击事件,通过匹配选择器(标准事件委托)或由任何其他任意要求(EG 是目标元素在另一个元素中)。
    • 这确实会引起@AllanHortle 指出的问题。 react 事件上的 stopPropagation 不会阻止文档事件处理程序接收事件。
    • 致所有感兴趣的人,我在使用文档时遇到了 stopPropagation 问题。如果您将事件侦听器附加到窗口,它似乎可以解决此问题?
    • 如前所述 this.getDOMNode() 已过时。像这样使用 ReactDOM:ReactDOM.findDOMNode(this).contains(e.target)
    【解决方案2】:

    查看事件的目标,如果事件直接在组件上,或者该组件的子组件上,那么点击在里面。否则它在外面。

    React.createClass({
        clickDocument: function(e) {
            var component = React.findDOMNode(this.refs.component);
            if (e.target == component || $(component).has(e.target).length) {
                // Inside of the component.
            } else {
                // Outside of the component.
            }
    
        },
        componentDidMount: function() {
            $(document).bind('click', this.clickDocument);
        },
        componentWillUnmount: function() {
            $(document).unbind('click', this.clickDocument);
        },
        render: function() {
            return (
                <div ref='component'>
                    ...
                </div> 
            )
        }
    });
    

    如果要在许多组件中使用它,最好使用 mixin:

    var ClickMixin = {
        _clickDocument: function (e) {
            var component = React.findDOMNode(this.refs.component);
            if (e.target == component || $(component).has(e.target).length) {
                this.clickInside(e);
            } else {
                this.clickOutside(e);
            }
        },
        componentDidMount: function () {
            $(document).bind('click', this._clickDocument);
        },
        componentWillUnmount: function () {
            $(document).unbind('click', this._clickDocument);
        },
    }
    

    在此处查看示例:https://jsfiddle.net/0Lshs7mg/1/

    【讨论】:

    • @Mike'Pomax'Kamermans 已经修复,我认为这个答案增加了有用的信息,也许你的评论现在可能会被删除。
    • 您出于错误的原因撤消了我的更改。 this.refs.component 指的是从 0.14 开始的 DOM 元素 facebook.github.io/react/blog/2015/07/03/…
    • @GajusKuizinas - 一旦 0.14 是最新版本(现在是测试版),就可以进行更改。
    • 什么是美元?
    • 我讨厌用 React 戳 jQuery,因为它们是两个视图的框架。
    【解决方案3】:

    在元素中我添加了mousedownmouseup,如下所示:

    onMouseDown={this.props.onMouseDown} onMouseUp={this.props.onMouseUp}
    

    然后在父级中我这样做:

    componentDidMount: function () {
        window.addEventListener('mousedown', this.pageClick, false);
    },
    
    pageClick: function (e) {
      if (this.mouseIsDownOnCalendar) {
          return;
      }
    
      this.setState({
          showCal: false
      });
    },
    
    mouseDownHandler: function () {
        this.mouseIsDownOnCalendar = true;
    },
    
    mouseUpHandler: function () {
        this.mouseIsDownOnCalendar = false;
    }
    

    showCal 是一个布尔值,当true 在我的例子中显示一个日历而false 隐藏它时。

    【讨论】:

    • 这会将点击专门与鼠标联系起来。点击事件也可以由触摸事件和输入键生成,此解决方案将无法响应,因此不适合移动和可访问性目的 =(
    • @Mike'Pomax'Kamermans 您现在可以在移动设备上使用 onTouchStart 和 onTouchEnd。 facebook.github.io/react/docs/events.html#touch-events
    • 那些已经存在很长时间了,但不会很好地与 android 配合使用,因为您需要立即在事件上调用 preventDefault() 或原生 Android 行为启动,React 的预处理会干扰.从那以后我写了npmjs.com/package/react-onclickoutside
    • 我喜欢它!称赞。在 mousedown 上删除事件侦听器会很有帮助。 componentWillUnmount = () => window.removeEventListener('mousedown', this.pageClick, false);
    【解决方案4】:
    1. 创建一个跨越整个屏幕的固定图层 (.backdrop)。
    2. 将目标元素 (.target) 放在 .backdrop 元素之外并具有更大的堆叠索引 (z-index)。

    那么对.backdrop 元素的任何点击都将被视为“在.target 元素之外”。

    .click-overlay {
        position: fixed;
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
        z-index: 1;
    }
    
    .target {
        position: relative;
        z-index: 2;
    }
    

    【讨论】:

      【解决方案5】:

      对于您的特定用例,当前接受的答案有点过度设计。如果您想在用户点击下拉列表时进行监听,只需使用 &lt;select&gt; 组件作为父元素并为其附加 onBlur 处理程序即可。

      这种方法的唯一缺点是它假定用户已经保持对元素的关注,并且它依赖于表单控件(如果您考虑到 tab key 还聚焦和模糊元素) - 但这些缺点只是对更复杂的用例的真正限制,在这种情况下可能需要更复杂的解决方案。

       var Dropdown = React.createClass({
      
         handleBlur: function(e) {
           // do something when user clicks outside of this element
         },
      
         render: function() {
           return (
             <select onBlur={this.handleBlur}>
               ...
             </select>
           );
         }
       });
      

      【讨论】:

      • onBlur div 也适用于具有tabIndex 属性的 div
      • 对我来说,这是我发现的最简单和最容易使用的解决方案,我在按钮元素上使用它,就像一个魅力。谢谢!
      【解决方案6】:

      我为源自组件外部的事件编写了一个通用事件处理程序,react-outside-event

      实现本身很简单:

      • 安装组件后,事件处理程序将附加到window 对象。
      • 发生事件时,组件会检查事件是否源自组件内部。如果没有,则在目标组件上触发onOutsideEvent
      • 卸载组件后,事件处理程序将被分离。
      import React from 'react';
      import ReactDOM from 'react-dom';
      
      /**
       * @param {ReactClass} Target The component that defines `onOutsideEvent` handler.
       * @param {String[]} supportedEvents A list of valid DOM event names. Default: ['mousedown'].
       * @return {ReactClass}
       */
      export default (Target, supportedEvents = ['mousedown']) => {
          return class ReactOutsideEvent extends React.Component {
              componentDidMount = () => {
                  if (!this.refs.target.onOutsideEvent) {
                      throw new Error('Component does not defined "onOutsideEvent" method.');
                  }
      
                  supportedEvents.forEach((eventName) => {
                      window.addEventListener(eventName, this.handleEvent, false);
                  });
              };
      
              componentWillUnmount = () => {
                  supportedEvents.forEach((eventName) => {
                      window.removeEventListener(eventName, this.handleEvent, false);
                  });
              };
      
              handleEvent = (event) => {
                  let target,
                      targetElement,
                      isInside,
                      isOutside;
      
                  target = this.refs.target;
                  targetElement = ReactDOM.findDOMNode(target);
                  isInside = targetElement.contains(event.target) || targetElement === event.target;
                  isOutside = !isInside;
      
      
      
                  if (isOutside) {
                      target.onOutsideEvent(event);
                  }
              };
      
              render() {
                  return <Target ref='target' {... this.props} />;
              }
          }
      };
      

      要使用该组件,您需要使用高阶组件包装目标组件类声明并定义您要处理的事件:

      import React from 'react';
      import ReactDOM from 'react-dom';
      import ReactOutsideEvent from 'react-outside-event';
      
      class Player extends React.Component {
          onOutsideEvent = (event) => {
              if (event.type === 'mousedown') {
      
              } else if (event.type === 'mouseup') {
      
              }
          }
      
          render () {
              return <div>Hello, World!</div>;
          }
      }
      
      export default ReactOutsideEvent(Player, ['mousedown', 'mouseup']);
      

      【讨论】:

        【解决方案7】:

        我对其中一个答案投了赞成票,尽管它对我不起作用。它最终导致我找到了这个解决方案。我稍微改变了操作顺序。我监听目标上的 mouseDown 和目标上的 mouseUp。如果其中任何一个返回 TRUE,我们就不会关闭模式。一旦注册了点击,在任何地方,这两个布尔值 { mouseDownOnModal, mouseUpOnModal } 都会被设置回 false。

        componentDidMount() {
            document.addEventListener('click', this._handlePageClick);
        },
        
        componentWillUnmount() {
            document.removeEventListener('click', this._handlePageClick);
        },
        
        _handlePageClick(e) {
            var wasDown = this.mouseDownOnModal;
            var wasUp = this.mouseUpOnModal;
            this.mouseDownOnModal = false;
            this.mouseUpOnModal = false;
            if (!wasDown && !wasUp)
                this.close();
        },
        
        _handleMouseDown() {
            this.mouseDownOnModal = true;
        },
        
        _handleMouseUp() {
            this.mouseUpOnModal = true;
        },
        
        render() {
            return (
                <Modal onMouseDown={this._handleMouseDown} >
                       onMouseUp={this._handleMouseUp}
                    {/* other_content_here */}
                </Modal>
            );
        }
        

        这样做的好处是所有代码都由子组件而不是父组件。这意味着在重用该组件时无需复制样板代码。

        【讨论】:

          【解决方案8】:

          使用优秀的react-onclickoutside mixin:

          npm install --save react-onclickoutside
          

          然后

          var Component = React.createClass({
            mixins: [
              require('react-onclickoutside')
            ],
            handleClickOutside: function(evt) {
              // ...handling code goes here... 
            }
          });
          

          【讨论】:

          • FYI mix-ins 现在在 React 中被弃用了。
          【解决方案9】:

          派对迟到了,但我已经成功地在下拉列表的父元素上设置了一个模糊事件,并使用相关代码关闭下拉列表,并将 mousedown 侦听器附加到父元素以检查是否下拉菜单是否打开,如果它打开,将停止事件传播,以免触发模糊事件。

          由于 mousedown 事件会冒泡,这将防止任何 mousedown 对子项造成对父项的模糊。

          /* Some react component */
          ...
          
          showFoo = () => this.setState({ showFoo: true });
          
          hideFoo = () => this.setState({ showFoo: false });
          
          clicked = e => {
              if (!this.state.showFoo) {
                  this.showFoo();
                  return;
              }
              e.preventDefault()
              e.stopPropagation()
          }
          
          render() {
              return (
                  <div 
                      onFocus={this.showFoo}
                      onBlur={this.hideFoo}
                      onMouseDown={this.clicked}
                  >
                      {this.state.showFoo ? <FooComponent /> : null}
                  </div>
              )
          }
          
          ...
          

          e.preventDefault() 不应该被调用,因为我可以推理,但是无论出于何种原因,没有它,Firefox 都不会玩得很好。适用于 Chrome、Firefox 和 Safari。

          【讨论】:

            【解决方案10】:

            您可以使用refs 来实现这一点,如下所示应该可以。

            ref 添加到您的元素中:

            <div ref={(element) => { this.myElement = element; }}></div>
            

            然后您可以添加一个函数来处理元素外部的点击,如下所示:

            handleClickOutside(e) {
              if (!this.myElement.contains(e)) {
                this.setState({ myElementVisibility: false });
              }
            }
            

            最后,在 will mount 和 will unmount 上添加和删除事件侦听器。

            componentWillMount() {
              document.addEventListener('click', this.handleClickOutside, false);  // assuming that you already did .bind(this) in constructor
            }
            
            componentWillUnmount() {
              document.removeEventListener('click', this.handleClickOutside, false);  // assuming that you already did .bind(this) in constructor
            }
            

            【讨论】:

            • 修改了您的答案,通过添加this 引用在document.addEventListener() 中调用handleClickOutside 可能存在错误。否则,它会给出 Uncaught ReferenceError: handleClickOutside is not defined in componentWillMount()
            【解决方案11】:

            我找到了一个更简单的方法。

            您只需要在模态框上添加onHide(this.closeFunction)

            <Modal onHide={this.closeFunction}>
            ...
            </Modal>
            

            假设你有一个关闭模式的功能。

            【讨论】:

              猜你喜欢
              • 2022-11-17
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-09-19
              • 1970-01-01
              相关资源
              最近更新 更多