Как прослушивать события щелчка вне компонента

89

Я хочу закрыть раскрывающееся меню, когда щелчок происходит за пределами раскрывающегося компонента.

Как я могу это сделать?

Аллан Хортл
источник

Ответы:

59

В элементе, который я добавил mousedownи mouseupвот так:

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скрывает его.

Робберт ван Элк
источник
однако это связывает щелчок именно с мышью. События щелчка могут быть сгенерированы событиями касания и клавишами ввода, на которые это решение не сможет реагировать, что делает его непригодным для мобильных устройств и целей доступности = (
Майк 'Pomax' Камерманс
@ 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 .
Майк 'Pomax' Камерманс
Я люблю это! Рекомендовано. Будет полезно удалить прослушиватель событий на mousedown. componentWillUnmount = () => window.removeEventListener ('mousedown', this.pageClick, false);
Джуни Бросас
73

Используя методы жизненного цикла, добавляйте и удаляйте прослушиватели событий в документ.

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

i_like_robots
источник
Это добавляет один в документ, но это означает, что любые события щелчка в компоненте также запускают событие документа. Это вызывает собственные проблемы, потому что целью слушателя документа всегда является какой-то нижний div в конце компонента, а не сам компонент.
Аллан Хортл
Я не совсем уверен, что следую за вашим комментарием, но если вы привяжете прослушиватели событий к документу, вы можете отфильтровать любые всплывающие до него события щелчка, либо сопоставив селектор (стандартное делегирование событий), либо любыми другими произвольными требованиями (например целевой элемент не в другом элементе).
i_like_robots
3
Это вызывает проблемы, как указывает @AllanHortle. stopPropagation для события реакции не помешает обработчикам событий документа получить событие.
Мэтт Кринкло-Фогт
4
Всем, кто интересовался, у меня были проблемы с stopPropagation при использовании документа. Если вы прикрепите прослушиватель событий к окну, кажется, эта проблема решена?
Titus
10
Как уже упоминалось, this.getDOMNode () устарел. используйте ReactDOM вместо этого: ReactDOM.findDOMNode (this) .contains (e.target)
Арне Х. Битубекк 07
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> 
        )
    }
});

Если это будет использоваться во многих компонентах, лучше использовать миксин:

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, который был исправлен, я думаю, что этот ответ добавляет полезную информацию, возможно, ваш комментарий теперь может быть удален.
ja
вы отменили мои изменения по неправильной причине. this.refs.componentотносится к элементу DOM по состоянию на 0.14 facebook.github.io/react/blog/2015/07/03/…
Gajus
@GajusKuizinas - можно внести это изменение, если 0.14 - последняя версия (сейчас бета).
JA
Что такое доллар?
pronebird
2
Ненавижу совать jQuery с React, потому что это фреймворк с двумя представлениями.
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 также работает с div, если у него есть tabIndexатрибут
gorpacrate
Для меня это было решение, которое я нашел самым простым и легким в использовании, я использую его для элемента кнопки и работает как шарм. Благодарность!
Amir5000,
5

Я написал универсальный обработчик событий для событий, которые происходят вне компонента, response-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']);
Gajus
источник
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
}
Крис Борроудейл
источник
Модифицированная свой ответ, он имел возможность ошибки в ссылке на вызов handleClickOutsideв document.addEventListener()путем добавления thisссылки. В противном случае выдает ошибку Uncaught ReferenceError: handleClickOutside не определен вcomponentWillMount()
Мухаммад Ханнан,
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>

Предполагая, что у вас есть функция для закрытия модального окна.

MR Murazza
источник
-1

Используйте отличный миксин react-onclickoutside :

npm install --save react-onclickoutside

А потом

var Component = React.createClass({
  mixins: [
    require('react-onclickoutside')
  ],
  handleClickOutside: function(evt) {
    // ...handling code goes here... 
  }
});
e18r
источник
4
FYI-вставки в React устарели.
machineghost