Come ascoltare gli eventi di clic esterni a un componente


89

Desidero chiudere un menu a discesa quando si verifica un clic al di fuori del componente a discesa.

Come lo faccio?

Risposte:


58

Nell'elemento che ho aggiunto mousedowne in mouseupquesto modo:

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

Quindi nel genitore faccio questo:

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

Il showCalè un booleano che quando truemostra nel mio caso un calendario e lo falsenasconde.


tuttavia, questo lega il clic in modo specifico a un mouse. Gli eventi di clic possono essere generati da eventi di tocco e anche da tasti di immissione, a cui questa soluzione non sarà in grado di reagire, rendendola inadatta per scopi mobili e di accessibilità = (
Mike 'Pomax' Kamermans

@ Mike'Pomax'Kamermans Ora puoi usare onTouchStart e onTouchEnd per dispositivi mobili. facebook.github.io/react/docs/events.html#touch-events
naoufal

3
Quelli esistono da molto tempo, ma non funzioneranno bene con Android, perché è necessario richiamare preventDefault()immediatamente gli eventi o il comportamento Android nativo entra in gioco, con cui la preelaborazione di React interferisce. Da allora ho scritto npmjs.com/package/react-onclickoutside .
Mike 'Pomax' Kamermans

Lo adoro! Lodato. Sarà utile rimuovere il listener di eventi su mousedown. componentWillUnmount = () => window.removeEventListener ('mousedown', this.pageClick, false);
Juni Brosas

73

Utilizzando i metodi del ciclo di vita, aggiungere e rimuovere listener di eventi dal documento.

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

Controlla le righe 48-54 di questo componente: https://github.com/i-like-robots/react-tube-tracker/blob/91dc0129a1f6077bef57ea4ad9a860be0c600e9d/app/component/tube-tracker.jsx#L48-54


Ciò aggiunge uno al documento, ma ciò significa che qualsiasi evento di clic sul componente attiva anche l'evento del documento. Ciò causa i suoi problemi perché l'obiettivo del listener del documento è sempre un div in basso alla fine di un componente, non il componente stesso.
Allan Hortle

Non sono del tutto sicuro di seguire il tuo commento, ma se leghi gli ascoltatori di eventi al documento puoi filtrare tutti gli eventi di clic che compaiono su di esso, abbinando un selettore (delega di eventi standard) o qualsiasi altro requisito arbitrario (EG era l'elemento di destinazione non all'interno di un altro elemento).
i_like_robots

3
Ciò causa problemi come sottolinea @AllanHortle. stopPropagation su un evento react non impedisce ai gestori di eventi del documento di ricevere l'evento.
Matt Crinklaw-Vogt

4
A chiunque fosse interessato stavo avendo problemi con stopPropagation durante l'utilizzo del documento. Se colleghi il tuo listener di eventi alla finestra, sembra che questo problema risolva?
Titus

10
Come accennato this.getDOMNode () è Obsoleto. usa ReactDOM invece in questo modo: ReactDOM.findDOMNode (this) .contains (e.target)
Arne H. Bitubekk

17

Guarda il target dell'evento, se l'evento era direttamente sul componente o sui figli di quel componente, il clic era all'interno. Altrimenti era fuori.

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

Se questo deve essere utilizzato in molti componenti, è più bello con un 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);
    },
}

Vedi esempio qui: https://jsfiddle.net/0Lshs7mg/1/


@ Mike'Pomax'Kamermans che è stato corretto, penso che questa risposta aggiunga informazioni utili, forse il tuo commento potrebbe ora essere rimosso.
ja

hai annullato le mie modifiche per un motivo sbagliato. this.refs.componentsi riferisce all'elemento DOM di 0.14 facebook.github.io/react/blog/2015/07/03/…
Gajus

@ GajusKuizinas - va bene apportare questa modifica una volta che la 0.14 è l'ultima versione (ora è beta).
ja

Cos'è il dollaro?
Ben Sinclair

2
Odio colpire jQuery con React poiché sono framework a due visualizzazioni.
Abdennour TOUMI

11

Per il tuo caso d'uso specifico, la risposta attualmente accettata è un po 'troppo ingegnerizzata. Se desideri ascoltare quando un utente fa clic su un elenco a discesa, utilizza semplicemente un <select>componente come elemento genitore e allega unonBlur gestore.

L'unico svantaggio di questo approccioèche presume che l'utente abbia già mantenuto il focus sull'elemento e si basa su un controllo del modulo (che può o meno essere quello che vuoi se si tiene conto che la tabchiave focalizza e sfoca anche gli elementi ) - ma questi inconvenienti sono solo un limite per casi d'uso più complicati, nel qual caso potrebbe essere necessaria una soluzione più complicata.

 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 funziona anche con div se ha tabIndexattributo
gorpacrate il

Per me questa è stata la soluzione che ho trovato più semplice e facile da usare, la sto usando su un elemento pulsante e funziona come un fascino. Grazie!
Amir5000

5

Ho scritto un gestore di eventi generico per eventi che hanno origine al di fuori del componente, reazione-esterno-evento .

L'implementazione stessa è semplice:

  • Quando il componente è montato, un gestore di eventi è collegato a window all'oggetto.
  • Quando si verifica un evento, il componente controlla se l'evento ha origine dall'interno del componente. Se non lo fa, si attivaonOutsideEvent sul componente di destinazione.
  • Quando il componente viene smontato, il gestore eventi viene scollegato.
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} />;
        }
    }
};

Per utilizzare il componente, è necessario racchiudere la dichiarazione della classe del componente di destinazione utilizzando il componente di ordine superiore e definire gli eventi che si desidera gestire:

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

Ho votato una delle risposte anche se non ha funzionato per me. Alla fine mi ha portato a questa soluzione. Ho cambiato leggermente l'ordine delle operazioni. Ascolto mouseDown sul bersaglio e mouseUp sul bersaglio. Se uno di questi restituisce TRUE, non chiudiamo il modale. Non appena viene registrato un clic, ovunque, quei due booleani {mouseDownOnModal, mouseUpOnModal} vengono reimpostati su 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>
    );
}

Questo ha il vantaggio che tutto il codice rimane con il componente figlio e non con il genitore. Significa che non c'è codice boilerplate da copiare quando si riutilizza questo componente.


3
  1. Crea un livello fisso che copre l'intero schermo ( .backdrop).
  2. Avere l'elemento di destinazione ( .target) all'esterno .backdropdell'elemento e con un indice di impilamento maggiore ( z-index).

Quindi qualsiasi clic .backdropsull'elemento verrà considerato "esterno .targetall'elemento".

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

.target {
    position: relative;
    z-index: 2;
}

2

Potresti usare ref s per raggiungere questo obiettivo, qualcosa come il seguente dovrebbe funzionare.

Aggiungi refal tuo elemento:

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

È quindi possibile aggiungere una funzione per gestire il clic all'esterno dell'elemento in questo modo:

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

Infine, aggiungi e rimuovi i listener di eventi su cui verranno montati e smontati.

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
}

Modificata la tua risposta, aveva possibile errore in riferimento a chiamare handleClickOutsidein document.addEventListener()con l'aggiunta di thisriferimento. Altrimenti restituisce Uncaught ReferenceError: handleClickOutside non è definito incomponentWillMount()
Muhammad Hannan

1

Molto tardi per la festa, ma ho avuto successo con l'impostazione di un evento di sfocatura sull'elemento genitore del menu a discesa con il codice associato per chiudere il menu a discesa e anche allegando un listener del mouse all'elemento genitore che controlla se il menu a discesa è aperto oppure no, e interromperà la propagazione dell'evento se è aperto in modo che l'evento di sfocatura non venga attivato.

Poiché l'evento mousedown si manifesta, questo impedirà a qualsiasi mousedown sui bambini di causare una sfocatura al genitore.

/* 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 () non dovrebbe essere chiamato per quanto posso ragionare, ma firefox non funziona bene senza di esso per qualsiasi motivo. Funziona su Chrome, Firefox e Safari.


0

Ho trovato un modo più semplice per farlo.

Hai solo bisogno di aggiungere onHide(this.closeFunction)il modale

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

Supponendo che tu abbia una funzione per chiudere il modal.


-1

Usa l'eccellente mixaggio di reazioni su clic :

npm install --save react-onclickoutside

E poi

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

4
FYI mix-in sono deprecati in React ora.
machineghost
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.