如何侦听组件外部的单击事件


89

当在下拉菜单组件之外单击时,我想关闭一个下拉菜单。

我怎么做?

Answers:


58

在元素我已经加入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

@ Mike'Pomax'Kamermans现在,您可以将onTouchStart和onTouchEnd用于移动设备。facebook.github.io/react/docs/events.html#touch-events
naoufal

3
这些已经存在很长时间了,但是在android上不能很好地发挥作用,因为您需要preventDefault()立即调用事件,否则将引发本机Android行为,而React的预处理会干扰这些行为。从那以后我写了npmjs.com/package/react-onclickoutside
Mike'Pomax'Kamermans

我喜欢它!表扬。在mousedown上删除事件侦听器将很有帮助。componentWillUnmount =()=> window.removeEventListener('mousedown',this.pageClick,false);
朱尼·布鲁萨斯

72

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

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,目标元素不在另一个元素之内)。
i_like_robots 2014年

3
正如@AllanHortle指出的那样,这确实会引起问题。react事件上的stopPropagation不会阻止文档事件处理程序接收该事件。
Matt Crinklaw-Vogt 2014年

4
对于感兴趣的任何人,在使用文档时我都会遇到stopPropagation的问题。如果将事件侦听器附加到窗口,似乎可以解决此问题?
泰特斯

10
如前所述,this.getDOMNode()已过时。改为使用ReactDOM:ReactDOM.findDOMNode(this).contains(e.target)
Arne H. Bitubekk '16

17

查看事件的目标,如果事件直接在组件上或该组件的子组件上,则单击位于内部。否则它在外面。

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已修复,我认为此答案添加了有用的信息,也许您的评论现在可能已删除。
2015年

您出于错误原因撤消了我的更改。this.refs.component指的DOM元素作为0.14的facebook.github.io/react/blog/2015/07/03/...
Gajus

@GajusKuizinas-可以在0.14是最新版本(现在是beta)后进行更改。
2015年

什么是美元?
本·辛克莱

2
我讨厌用React戳jQuery,因为它们是两个视图的框架。
Abdennour TOUMI

11

对于您的特定用例,当前接受的答案是经过精心设计的。如果您想在用户单击下拉列表时进行监听,只需将<select>组件用作父元素并onBlur为其添加处理程序即可。

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

 var Dropdown = React.createClass({

   handleBlur: function(e) {
     // do something when user clicks outside of this element
   },

   render: function() {
     return (
       <select onBlur={this.handleBlur}>
         ...
       </select>
     );
   }
 });

10
onBlurdiv如果具有tabIndex属性,也可与div一起使用
gorpacrate

对我来说,这是我发现最简单,最容易使用的解决方案,我在按钮元素上使用它,就像是一种魅力。谢谢!
Amir5000

5

我已经为源自组件外部的事件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']);

4

即使它对我没有用,我还是投票了一个答案。最终导致我找到了这个解决方案。我稍微改变了操作顺序。我在目标上监听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>
    );
}

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


3
  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;
}

2

您可以使用refs实现此目的,类似以下的内容应该起作用。

将添加ref到您的元素:

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

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

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

最后,添加和删除事件侦听器,将挂载和卸载。

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
}

修改你的答案,它在参考可能出现的错误调用handleClickOutsidedocument.addEventListener()通过添加this引用。否则,它将导致未捕获的ReferenceError:handleClickOutside未componentWillMount()
Muhammad Hannan

1

派对晚了,但是我成功地在下拉菜单的父元素上设置了一个模糊事件,并使用关联的代码关闭了下拉菜单,并且还将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。


0

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

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

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

假设您具有关闭模态的功能。


-1

使用出色的react-onclickoutside混合功能:

npm install --save react-onclickoutside

然后

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

4
FYI混入现在已在React中弃用。
machineghost
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.