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

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

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


当前回答

战略示例

我喜欢所提供的解决方案,这些解决方案通过围绕组件创建包装器来完成相同的任务。

由于这更多是一种行为,我想到了战略,并提出了以下建议。

我是React的新手,我需要一些帮助来保存用例中的样板

请回顾并告诉我你的想法。

ClickOutside行为

import ReactDOM from 'react-dom';

export default class ClickOutsideBehavior {

  constructor({component, appContainer, onClickOutside}) {

    // Can I extend the passed component's lifecycle events from here?
    this.component = component;
    this.appContainer = appContainer;
    this.onClickOutside = onClickOutside;
  }

  enable() {

    this.appContainer.addEventListener('click', this.handleDocumentClick);
  }

  disable() {

    this.appContainer.removeEventListener('click', this.handleDocumentClick);
  }

  handleDocumentClick = (event) => {

    const area = ReactDOM.findDOMNode(this.component);

    if (!area.contains(event.target)) {
        this.onClickOutside(event)
    }
  }
}

示例用法

import React, {Component} from 'react';
import {APP_CONTAINER} from '../const';
import ClickOutsideBehavior from '../ClickOutsideBehavior';

export default class AddCardControl extends Component {

  constructor() {
    super();

    this.state = {
      toggledOn: false,
      text: ''
    };

    this.clickOutsideStrategy = new ClickOutsideBehavior({
      component: this,
      appContainer: APP_CONTAINER,
      onClickOutside: () => this.toggleState(false)
    });
  }

  componentDidMount () {

    this.setState({toggledOn: !!this.props.toggledOn});
    this.clickOutsideStrategy.enable();
  }

  componentWillUnmount () {
    this.clickOutsideStrategy.disable();
  }

  toggleState(isOn) {

    this.setState({toggledOn: isOn});
  }

  render() {...}
}

笔记

我想到了存储传递的组件生命周期挂钩,并用类似的方法覆盖它们:

const baseDidMount = component.componentDidMount;

component.componentDidMount = () => {
  this.enable();
  baseDidMount.call(component)
}

component是传递给ClickOutsideBehavior构造函数的组件。这将从该行为的用户中删除启用/禁用样板,但看起来不太好

其他回答

这是我的方法(演示-https://jsfiddle.net/agymay93/4/):

我创建了一个名为WatchClickOutside的特殊组件,它可以像这样使用(我假设JSX语法):

<WatchClickOutside onClickOutside={this.handleClose}>
  <SomeDropdownEtc>
</WatchClickOutside>

以下是WatchClickOutside组件的代码:

import React, { Component } from 'react';

export default class WatchClickOutside extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  componentWillMount() {
    document.body.addEventListener('click', this.handleClick);
  }

  componentWillUnmount() {
    // remember to remove all events to avoid memory leaks
    document.body.removeEventListener('click', this.handleClick);
  }

  handleClick(event) {
    const {container} = this.refs; // get container that we'll wait to be clicked outside
    const {onClickOutside} = this.props; // get click outside callback
    const {target} = event; // get direct click event target

    // if there is no proper callback - no point of checking
    if (typeof onClickOutside !== 'function') {
      return;
    }

    // if target is container - container was not clicked outside
    // if container contains clicked target - click was not outside of it
    if (target !== container && !container.contains(target)) {
      onClickOutside(event); // clicked outside - fire callback
    }
  }

  render() {
    return (
      <div ref="container">
        {this.props.children}
      </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>

这已经有很多答案了,但它们没有解决e.stopPropagation()和阻止单击要关闭的元素之外的react链接的问题。

由于React有自己的人工事件处理程序,您无法将文档用作事件侦听器的基础。在这之前,您需要e.stopPropagation(),因为React使用文档本身。如果改用document.querySelector('body')。您可以防止点击React链接。下面是我如何实现单击外部并关闭的示例。这使用ES6和React 16.3。

import React, { Component } from 'react';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      isOpen: false,
    };

    this.insideContainer = React.createRef();
  }

  componentWillMount() {
    document.querySelector('body').addEventListener("click", this.handleClick, false);
  }

  componentWillUnmount() {
    document.querySelector('body').removeEventListener("click", this.handleClick, false);
  }

  handleClick(e) {
    /* Check that we've clicked outside of the container and that it is open */
    if (!this.insideContainer.current.contains(e.target) && this.state.isOpen === true) {
      e.preventDefault();
      e.stopPropagation();
      this.setState({
        isOpen: false,
      })
    }
  };

  togggleOpenHandler(e) {
    e.preventDefault();

    this.setState({
      isOpen: !this.state.isOpen,
    })
  }

  render(){
    return(
      <div>
        <span ref={this.insideContainer}>
          <a href="#open-container" onClick={(e) => this.togggleOpenHandler(e)}>Open me</a>
        </span>
        <a href="/" onClick({/* clickHandler */})>
          Will not trigger a click when inside is open.
        </a>
      </div>
    );
  }
}

export default App;

我使用了这个模块(我与作者没有关联)

npm install react-onclickout --save

const ClickOutHandler=require('act-on-clickout');ExampleComponent类扩展React.Component{单击退出(e){if(hasClass(e.target,'忽略我'))返回;alert('用户在组件外部单击!');}render(){返回(<ClickOutHandler onClickOut={this.onClickOut}><div>点击我的外部</分区></ClickOutHandler>);}}

它做得很好。

@ford04提案的字体+简化版:

使用OuterClick API

const Client = () => {
  const ref = useOuterClick<HTMLDivElement>(e => { /* Custom-event-handler */ });
  return <div ref={ref}> Inside </div> 
};

实施

export default function useOuterClick<T extends HTMLElement>(callback: Function) {
  const callbackRef = useRef<Function>(); // initialize mutable ref, which stores callback
  const innerRef = useRef<T>(null); // returned to client, who marks "border" element

  // update cb on each render, so second useEffect has access to current value
  useEffect(() => { callbackRef.current = callback; });

  useEffect(() => {
    document.addEventListener("click", _onClick);
    return () => document.removeEventListener("click", _onClick);
    function _onClick(e: any): void {
      const clickedOutside = !(innerRef.current?.contains(e.target));
      if (clickedOutside)
        callbackRef.current?.(e);
    }
  }, []); // no dependencies -> stable click listener

  return innerRef; // convenience for client (doesn't need to init ref himself)
}