我在制作可点击的Dropdown
组件时遇到了问题。我的任务是单击一个按钮时显示一个菜单,而当用户单击文档中的任意位置或是否单击同一按钮时,则隐藏菜单,所有组件也应该是功能组件。
我正在使用名为classnames
的第三方软件包,该软件包有助于有条件地加入CSS
类,还使用React ContextAPI
将道具传递给Dropdown
子组件。
Dropdown
组件取决于2个子组件。
DropdownToggle- (呈现可点击的按钮)
DropdownMenu- (使div带有菜单项)
问题:
每当我打开菜单并单击文档菜单中的任何位置时,效果都很好,但是当我打开菜单并想用按钮隐藏时,单击它不起作用。我认为问题出在useEffect
组件的Dropdown
挂钩内。
演示:
这里是主要的App
组件,呈现所有组件。
App.js
import React, { Component } from "react";
import Dropdown from "./Dropdown";
import DropdownToggle from "./DropdownToggle";
import DropdownMenu from "./DropdownMenu";
import "./dropdown.css";
// App component
class App extends Component {
state = {
isOpen: false
};
toggle = () => {
alert("Button is clicked");
this.setState({
isOpen: !this.state.isOpen
});
};
render() {
return (
<div className="app">
<Dropdown isOpen={this.state.isOpen} toggle={this.toggle}>
<DropdownToggle>Dropdown</DropdownToggle>
<DropdownMenu>
<div>Item 1</div>
<div>Item 2</div>
</DropdownMenu>
</Dropdown>
</div>
);
}
}
export default App;
主要 src 代码:
DropdownContext.js
import {createContext} from 'react';
// It is used on child components.
export const DropdownContext = createContext({});
// Wrap Dropdown with this Provider.
export const DropdownProvider = DropdownContext.Provider;
Dropdown.js
import React, { useEffect } from "react";
import classNames from "classnames";
import { DropdownProvider } from "./DropdownContext";
/**
* Returns a new object with the key/value pairs from `obj` that are not in the array `omitKeys`.
* @param obj
* @param omitKeys
*/
const omit = (obj, omitKeys) => {
const result = {};
// Get object properties as an array
const propsArray = Object.keys(obj);
propsArray.forEach(key => {
// Searches the array for the specified item, if the item is not found it returns -1 then
// construct a new object and return it.
if (omitKeys.indexOf(key) === -1) {
result[key] = obj[key];
}
});
return result;
};
// Dropdown component
const Dropdown = props => {
// Populate context value based on the props
const getContextValue = () => {
return {
toggle: props.toggle,
isOpen: props.isOpen
};
};
// toggle function
const toggle = e => {
// Execute toggle function which is came from the parent component
return props.toggle(e);
};
// handle click for the document object
const handleDocumentClick = e => {
// Execute toggle function of the parent
toggle(e);
};
// Remove event listeners
const removeEvents = () => {
["click", "touchstart"].forEach(event =>
document.removeEventListener(event, handleDocumentClick, true)
);
};
// Add event listeners
const addEvents = () => {
["click", "touchstart"].forEach(event =>
document.addEventListener(event, handleDocumentClick, true)
);
};
useEffect(() => {
const handleProps = () => {
if (props.isOpen) {
addEvents();
} else {
removeEvents();
}
};
// mount
handleProps();
// unmount
return () => {
removeEvents();
};
}, [props.isOpen]);
// Condense all other attributes except toggle `prop`.
const { className, isOpen, ...attrs } = omit(props, ["toggle"]);
// Conditionally join all classes
const classes = classNames(className, "dropdown", { show: isOpen });
return (
<DropdownProvider value={getContextValue()}>
<div className={classes} {...attrs} />
</DropdownProvider>
);
};
export default Dropdown;
Dropdown
组件有一个父组件,即Provider
会在Provider
值更改子组件时访问这些值。
其次,在DOM上它将呈现一个div
标记结构,该标记结构由Dropdown
组成。
DropdownToggle.js
import React, {useContext} from 'react';
import classNames from 'classnames';
import {DropdownContext} from './DropdownContext';
// DropdownToggle component
const DropdownToggle = (props) => {
const {toggle} = useContext(DropdownContext);
const onClick = (e) => {
// If props onClick is not undefined
if (props.onClick) {
// execute the function
props.onClick(e);
}
toggle(e);
};
const {className, ...attrs} = props;
const classes = classNames(className);
return (
// All children would be render inside this. e.g. `svg` & `text`
<button type="button" className={classes} onClick={onClick} {...attrs}/>
);
};
export default DropdownToggle;
DropdownMenu.js
import React, { useContext } from "react";
import classNames from "classnames";
import { DropdownContext } from "./DropdownContext";
// DropdownMenu component
const DropdownMenu = props => {
const { isOpen } = useContext(DropdownContext);
const { className, ...attrs } = props;
// add show class if isOpen is true
const classes = classNames(className, "dropdown-menu", { show: isOpen });
return (
// All children would be render inside this `div`
<div className={classes} {...attrs} />
);
};
export default DropdownMenu;
答案 0 :(得分:0)
切换功能链接到文档和按钮本身。因此,当您单击按钮时,它将触发一次,然后事件冒泡到document
并再次触发。必须小心将事件侦听器附加到整个document
对象。在您的Dropdown.js
文件中添加一行以停止事件传播:
// toggle function
const toggle = e => {
// Execute toggle function which is came from the parent component
e.stopPropagation(); // this stops it bubbling up to the document and firing again
return props.toggle(e);
};
答案 1 :(得分:0)
Jayce444
的答案是正确的。当您单击按钮时,它将触发一次,然后事件冒泡到文档并再次触发。
我只想为您添加另一个替代解决方案。您可以使用useRef
钩子来创建Dropdown
节点的引用,并检查当前事件目标是否为button
元素。将此代码添加到您的Dropdown.js
文件中。
import React, { useRef } from "react";
const Dropdown = props => {
const containerRef = useRef(null);
// get reference of the current div
const getReferenceDomNode = () => {
return containerRef.current;
};
// handle click for the document object
const handleDocumentClick = e => {
const container = getReferenceDomNode();
if (container.contains(e.target) && container !== e.target) {
return;
}
toggle(e);
};
//....
return (
<DropdownProvider value={getContextValue()}>
<div className={classes} {...attrs} ref={containerRef} />
</DropdownProvider>
);
};
export default Dropdown;