我正在寻找一种方法来检测单击事件是否发生在组件之外,如本文所述。jQueryclosest()用于查看单击事件的目标是否将dom元素作为其父元素之一。如果存在匹配项,则单击事件属于其中一个子项,因此不被视为在组件之外。
因此,在我的组件中,我想将一个单击处理程序附加到窗口。当处理程序启动时,我需要将目标与组件的dom子级进行比较。
click事件包含类似“path”的财产,它似乎保存了事件经过的dom路径。我不知道该比较什么,或者如何最好地遍历它,我想肯定有人已经把它放在了一个聪明的效用函数中。。。不
@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)
}
以下解决方案使用ES6并遵循绑定以及通过方法设置ref的最佳实践。
要将其付诸行动:
挂钩实施反应16.3后的类实现反应16.3之前的类实现
挂钩实施:
import React, { useRef, useEffect } from "react";
/**
* Hook that alerts clicks outside of the passed ref
*/
function useOutsideAlerter(ref) {
useEffect(() => {
/**
* Alert if clicked on outside of element
*/
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
alert("You clicked outside of me!");
}
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
}
/**
* Component that alerts if you click outside of it
*/
export default function OutsideAlerter(props) {
const wrapperRef = useRef(null);
useOutsideAlerter(wrapperRef);
return <div ref={wrapperRef}>{props.children}</div>;
}
类实现:
16.3之后
import React, { Component } from "react";
/**
* Component that alerts if you click outside of it
*/
export default class OutsideAlerter extends Component {
constructor(props) {
super(props);
this.wrapperRef = React.createRef();
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 (this.wrapperRef && !this.wrapperRef.current.contains(event.target)) {
alert("You clicked outside of me!");
}
}
render() {
return <div ref={this.wrapperRef}>{this.props.children}</div>;
}
}
16.3之前
import React, { Component } from "react";
/**
* Component that alerts if you click outside of it
*/
export default class OutsideAlerter extends Component {
constructor(props) {
super(props);
this.setWrapperRef = this.setWrapperRef.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
}
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
/**
* Set the wrapper ref
*/
setWrapperRef(node) {
this.wrapperRef = node;
}
/**
* Alert if clicked on outside of element
*/
handleClickOutside(event) {
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
alert("You clicked outside of me!");
}
}
render() {
return <div ref={this.setWrapperRef}>{this.props.children}</div>;
}
}
这是我的方法(演示-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>
我为所有场合制定了解决方案。
你应该使用一个高阶组件来包装你想要监听的组件。
这个组件示例只有一个属性:“onClickedOutside”,它接收函数。
ClickedOutside.js
import React, { Component } from "react";
export default class ClickedOutside extends Component {
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
handleClickOutside = event => {
// IF exists the Ref of the wrapped component AND his dom children doesnt have the clicked component
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
// A props callback for the ClikedClickedOutside
this.props.onClickedOutside();
}
};
render() {
// In this piece of code I'm trying to get to the first not functional component
// Because it wouldn't work if use a functional component (like <Fade/> from react-reveal)
let firstNotFunctionalComponent = this.props.children;
while (typeof firstNotFunctionalComponent.type === "function") {
firstNotFunctionalComponent = firstNotFunctionalComponent.props.children;
}
// Here I'm cloning the element because I have to pass a new prop, the "reference"
const children = React.cloneElement(firstNotFunctionalComponent, {
ref: node => {
this.wrapperRef = node;
},
// Keeping all the old props with the new element
...firstNotFunctionalComponent.props
});
return <React.Fragment>{children}</React.Fragment>;
}
}