我正在寻找一种方法来检测单击事件是否发生在组件之外,如本文所述。jQueryclosest()用于查看单击事件的目标是否将dom元素作为其父元素之一。如果存在匹配项,则单击事件属于其中一个子项,因此不被视为在组件之外。

因此,在我的组件中,我想将一个单击处理程序附加到窗口。当处理程序启动时,我需要将目标与组件的dom子级进行比较。

click事件包含类似“path”的财产,它似乎保存了事件经过的dom路径。我不知道该比较什么,或者如何最好地遍历它,我想肯定有人已经把它放在了一个聪明的效用函数中。。。不


当前回答

以上答案中没有一个对我有效,所以我最终做了如下:

从“React”导入React,{Component};

/**
 * Component that alerts if you click outside of it
 */
export default class OutsideAlerter extends Component {
  constructor(props) {
    super(props);

    this.handleClickOutside = this.handleClickOutside.bind(this);
  }

  componentDidMount() {
    document.addEventListener('mousedown', this.handleClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener('mousedown', this.handleClickOutside);
  }

  /**
   * Alert if clicked on outside of element
   */
  handleClickOutside(event) {
    if (!event.path || !event.path.filter(item => item.className=='classOfAComponent').length) {
      alert('You clicked outside of me!');
    }
  }

  render() {
    return <div>{this.props.children}</div>;
  }
}

OutsideAlerter.propTypes = {
  children: PropTypes.element.isRequired,
};

其他回答

[更新]使用挂钩的React ^16.8解决方案

代码沙盒

import React, { useEffect, useRef, useState } from 'react';

const SampleComponent = () => {
    const [clickedOutside, setClickedOutside] = useState(false);
    const myRef = useRef();

    const handleClickOutside = e => {
        if (!myRef.current.contains(e.target)) {
            setClickedOutside(true);
        }
    };

    const handleClickInside = () => setClickedOutside(false);

    useEffect(() => {
        document.addEventListener('mousedown', handleClickOutside);
        return () => document.removeEventListener('mousedown', handleClickOutside);
    });

    return (
        <button ref={myRef} onClick={handleClickInside}>
            {clickedOutside ? 'Bye!' : 'Hello!'}
        </button>
    );
};

export default SampleComponent;

反应溶液^16.3:

代码沙盒

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;

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

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

import onClickOutside from 'react-onclickoutside'

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

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

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

export default onClickOutside(NameOfComponent)

就是这样。

componentWillMount(){

  document.addEventListener('mousedown', this.handleClickOutside)
}

handleClickOutside(event) {

  if(event.path[0].id !== 'your-button'){
     this.setState({showWhatever: false})
  }
}

事件路径[0]是最后单击的项目

这是我解决问题的方法

我从我的自定义钩子返回一个布尔值,当这个值发生变化时(如果单击在我作为参数传递的ref之外,则为true),这样我就可以用useEffect钩子捕捉到这个变化,我希望你能清楚。

下面是一个活生生的例子:codesandbox上的实时示例

import { useEffect, useRef, useState } from "react";

const useOutsideClick = (ref) => {
  const [outsieClick, setOutsideClick] = useState(null);

  useEffect(() => {
    const handleClickOutside = (e) => {
      if (!ref.current.contains(e.target)) {
        setOutsideClick(true);
      } else {
        setOutsideClick(false);
      }

      setOutsideClick(null);
    };

    document.addEventListener("mousedown", handleClickOutside);

    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [ref]);

  return outsieClick;
};

export const App = () => {
  const buttonRef = useRef(null);
  const buttonClickedOutside = useOutsideClick(buttonRef);

  useEffect(() => {
    // if the the click was outside of the button
    // do whatever you want
    if (buttonClickedOutside) {
      alert("hey you clicked outside of the button");
    }
  }, [buttonClickedOutside]);

  return (
    <div className="App">
      <button ref={buttonRef}>click outside me</button>
    </div>
  );
}

非侵入性方式无需添加另一个DIV EL。

注意:React可能会说findDomNode已弃用,但到目前为止,我还没有遇到任何问题

@异常:单击要忽略的类

@idException:单击时忽略的id

import React from "react"
import ReactDOM from "react-dom"
type Func1<T1, R> = (a1: T1) => R


export function closest(
    el: Element,
    fn: (el: Element) => boolean
  ): Element | undefined {
    let el_: Element | null = el;
  
    while (el_) {
      if (fn(el_)) {
        return el_;
      }
  
      el_ = el_.parentElement;
    }
  }
let instances: ClickOutside[] = []

type Props = {
  idException?: string,
  exceptions?: (string | Func1<MouseEvent, boolean>)[]
  handleClickOutside: Func1<MouseEvent, void>

}


export default class ClickOutside extends React.Component<Props> {
  static defaultProps = {
    exceptions: []
  };

  componentDidMount() {
    if (instances.length === 0) {
      document.addEventListener("mousedown", this.handleAll, true)
      window.parent.document.addEventListener(
        "mousedown",
        this.handleAll,
        true
      )
    }
    instances.push(this)
  }

  componentWillUnmount() {
    instances.splice(instances.indexOf(this), 1)
    if (instances.length === 0) {
      document.removeEventListener("mousedown", this.handleAll, true)
      window.parent.document.removeEventListener(
        "mousedown",
        this.handleAll,
        true
      )
    }
  }

  handleAll = (e: MouseEvent) => {

    const target: HTMLElement = e.target as HTMLElement
    if (!target) return

    instances.forEach(instance => {
      const { exceptions, handleClickOutside: onClickOutside, idException } = instance.props as Required<Props>
      let exceptionsCount = 0

      if (exceptions.length > 0) {
        const { functionExceptions, stringExceptions } = exceptions.reduce(
          (acc, exception) => {
            switch (typeof exception) {
              case "function":
                acc.functionExceptions.push(exception)
                break
              case "string":
                acc.stringExceptions.push(exception)
                break
            }

            return acc
          },
          { functionExceptions: [] as Func1<MouseEvent, boolean>[], stringExceptions: [] as string[] }
        )
        if (functionExceptions.length > 0) {
          exceptionsCount += functionExceptions.filter(
            exception => exception(e) === true
          ).length
        }

        if (exceptionsCount === 0 && stringExceptions.length > 0) {

          const el = closest(target, (e) => stringExceptions.some(ex => e.classList.contains(ex)))
          if (el) {
            exceptionsCount++
          }
        }
      }

      if (idException) {
        const target = e.target as HTMLDivElement
        if (document.getElementById(idException)!.contains(target)) {
          exceptionsCount++
        }
      }

      if (exceptionsCount === 0) {
        // eslint-disable-next-line react/no-find-dom-node
        const node = ReactDOM.findDOMNode(instance)

        if (node && !node.contains(target)) {
          onClickOutside(e)
        }
      }
    })
  };

  render() {
    return React.Children.only(this.props.children)
  }
}

用法

<ClickOutside {...{ handleClickOutside: () => { alert('Clicked Outside') } }}>
    <div >
        <div>Breathe</div>
    </div>
</ClickOutside>