React.js Dropdown component doesn't hide on button click - reactjs

I'm facing a problem in the making of clickable Dropdown component. My task is to show a menu when a button is clicked and hide the menu when the user clicks anywhere in the document or whether a click on the same button, also all components should be functional components.
I'm using 3rd party package named classnames which help to conditionally join CSS classes, also using a React ContextAPI to pass props to Dropdown child components.
Dropdown component depends upon 2 child components.
DropdownToggle -
(Renders a clickable button)
DropdownMenu -
(Renders a div with menu items)
Problem:
Whenever I open a menu and click anywhere in the document menu works perfectly, but when I open a menu and want to hide with a button click it didn't work. I think the problem is inside the useEffect hook of the Dropdown component.
Codesandbox
Demo:
Here is the main App component which renders all components.
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;
Main src code:
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 component has a parent i.e. a Provider whenever Provider values will change child components will access those values.
Secondly, on the DOM it will render a div which consists of Dropdown markup structure.
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;

Jayce444 answer is correct. When you click the button, it fires once, then the event bubbles up to the document and fires again.
I just want to add another alternative solution for you. You can use useRef hook to create a reference of Dropdown node and check if the current event target is button element or not. Add this code to your Dropdown.js file.
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;

The toggling function is linked to both the document, and the button itself. So when you click the button, it fires once, then the event bubbles up to the document and fires again. Gotta be careful attaching event listeners to the entire document object. Add a line to stop the event propagation in your Dropdown.js file:
// 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);
};

Related

Is it possible to make the HTML inside Tippy clickable?

I'm working on a file sharing app. In the dashboard, I want to right click then a context menu appears and I can click on one of the options available in it. I can now display the context menu but it seems I can't click any button or element inside the menu.
Menu Component:
import Tippy from "#tippyjs/react/headless"
import { cloneElement, useState } from "react"
import { useLocation } from "react-router-dom"
import { followCursor } from "tippy.js"
import "tippy.js/dist/tippy.css"
import Options from "./Options"
type SubMenuProps = {
children: React.ReactElement
}
const NO_SUBMENU_PATHS = ["/", "/login", "/signup"]
const SubMenu = ({ children }: SubMenuProps) => {
const [visible, setVisible] = useState(false)
const { pathname } = useLocation()
const child = cloneElement(children, {
onContextMenu: (e: Event) => {
e.preventDefault()
setVisible(!visible)
},
})
if (NO_SUBMENU_PATHS.includes(pathname)) return <>{child}</>
return (
<Tippy
allowHTML
visible={visible}
onClickOutside={() => setVisible(false)}
render={Options}
followCursor="initial"
placement="right-start"
plugins={[followCursor]}
>
{child}
</Tippy>
)
}
export default SubMenu
I tried reading the docs and looking up online but found nothing helpful

How do I get a state variable from child to parent?

I am learning React.
I have Component structure like this -
index.js
import React from "react";
import Button from "./Button/Button"
export default function Index() {
return (
<>
<Button />
<div>Value of flag in Index.js = {}</div>
</>
);
}
Button.js
import React, { useState } from "react";
import "./button.css";
export default function Button(props) {
const [flag, setFlag] = useState(true);
const clickHandler = () => {
setFlag(!flag);
};
return (
<div className="btn" onClick={clickHandler}>
Value of flag in Button.js = {flag.toString()}
</div>
);
}
My question is "How do I get flag value from Button.js to index.js" ? (child to parent).
1) You can lift state in parent component and pass state and handler as a prop to children.
Note: This is will work because you need flag in the JSX, But if you will pass event handler as a prop in the child component then you have to invoke the handler to get the value. So Either lift the state or use Redux
Live Demo
App.js
const App = () => {
const [flag, setFlag] = useState( true );
return (
<>
<Button flag={flag} setFlag={setFlag} />
<div>Value of flag in Index.js = { flag.toString() }</div>
</>
);
};
Button.js
export default function Button({ flag, setFlag }) {
const clickHandler = () => {
setFlag(oldFlag => !oldFlag);
};
return (
<div className="btn" onClick={clickHandler}>
Value of flag in Button.js = {flag.toString()}
</div>
);
}
2) You can pass handler as a prop in child component as shown in the Harsh Patel answer
3) You can use state management tool i.e. Redux.
You can send a value by method, refer to this:
index.js
import React from "react";
import Button from "./Button/Button"
export default function Index() {
let getFlagValue = (flag) => {
//here you'll get a flag value
console.log(flag)
}
return (
<>
<Button sendFlagValue=(getFlagValue)/>
<div>Value of flag in Index.js = {}</div>
</>
);
}
Button.js
import React, { useState } from "react";
import "./button.css";
export default function Button(sendFlagValue) {
const [flag, setFlag] = useState(true);
const clickHandler = () => {
setFlag(!flag);
sendFlagValue(flag)
};
return (
<div className="btn" onClick={clickHandler}>
Value of flag in Button.js = {flag.toString()}
</div>
);
}
There are two types state:
global state for all
private state for component
For starter, you must obey some policies, not try abuse state, otherwise you will have some troubles.
For global STATE, you can use Redux or dva or umijs.
For private STATE, you seems already known.

React: A service returning a ui component (like toast)?

Requirement: Show toast on bottom-right corner of the screen on success/error/warning/info.
I can create a toast component and place it on any component where I want to show toasts, but this requires me to put Toast component on every component where I intend to show toasts. Alternatively I can place it on the root component and somehow manage show/hide (maintain state).
What I am wondering is having something similar to following
export class NotificationService {
public notify = ({message, notificationType, timeout=5, autoClose=true, icon=''}: Notification) => {
let show: boolean = true;
let onClose = () => {//do something};
if(autoClose) {
//set timeout
}
return show ? <Toast {...{message, notificationType, onClose, icon}} /> : </>;
}
}
And call this service where ever I need to show toasts.
Would this be the correct way to achieve the required functionality?
You can use AppContext to manage the state of your toast and a hook to trigger it whenever you want.
ToastContext:
import React, { createContext, useContext, useState } from 'react';
export const ToastContext = createContext();
export const useToastState = () => {
return useContext(ToastContext);
};
export default ({ children }) => {
const [toastState, setToastState] = useState(false);
const toastContext = { toastState, setToastState };
return <ToastContext.Provider value={toastContext}>{children}</ToastContext.Provider>;
};
App:
<ToastProvider>
<App/>
<Toast show={toastState}/>
</ToastProvider>
Then anywhere within your app you can do:
import {useToastState} from 'toastContext'
const {toastState, setToastState} = useToastState();
setToastState(!toastState);

On click returns null instead of an object

It's really basic I guess. I'm trying to add onClick callback to my script & I believe I'm missing a value that would be responsible for finding the actual item.
Main script
import React from 'react';
import { CSVLink } from 'react-csv';
import { data } from 'constants/data';
import GetAppIcon from '#material-ui/icons/GetApp';
import PropTypes from 'prop-types';
const handleClick = (callback) => {
callback(callback);
};
const DownloadData = (props) => {
const { callback } = props;
return (
<>
<CSVLink
data={data}
onClick={() => handleClick(callback)}
>
<GetAppIcon />
</CSVLink>
</>
);
};
DownloadData.propTypes = {
callback: PropTypes.func.isRequired,
};
export default DownloadData;
Storybook code
import React from 'react';
import DownloadData from 'common/components/DownloadData';
import { data } from 'constants/data';
import { action } from '#storybook/addon-actions';
export default {
title: 'DownloadData',
component: DownloadData,
};
export const download = () => (
<DownloadData
data={data}
callback={action('icon-clicked')}
/>
);
So right now with this code on click in the storybook I'd get null and I'm looking for an object.
One of the potential issues I can see is that your handleClick function is stored as it is in-memory, when you import the component. That means you're keeping reference of something that doesn't exists and expects to use it when rendering the component with the callback prop.
Each instance of a component should have its own function. To fix it, move the function declaration inside the component. Like this:
const Foo = ({ callback }) => {
// handleClick needs to be inside here
const handleClick = callback => {
console.log("clicked");
callback(callback);
};
return <div onClick={() => handleClick(callback)}>Click me!</div>;
};
Check this example.
If this doesn't fix your problem, then there is something wrong with how you're implementing Storybook. Like a missing context.

Reactjs refresh parent after react modal popup changes data

I am using react modal to set up a pop up in on one of my components. I have a component that renders a div wrapped with react modal. My parent component renders the modal component on load with the isOpen set to false. Clicking a link on the parent sets isOpen to true and causes the modal popup to open.
I make changes to my data within the open popup and then save the changes and close the model when the close button is clicked.
I am using a redux set up with actions and reducers handling the data changes and state changes.
Conceptually, how would I update the parent to show the changes made from the popup? Shouldn't changes to my data using an action cause the store to regenerate and hence update the data in my components or do I have to explicitly "refresh" my store after saving? My issue is that right now when the popup closes it is not tirggering any kind of "refresh" on the DataView component.
Components below:
DataView component
import React from 'react';
import DataView from './MonthView.js';
import DataViewPopup from './MonthViewPopup.js';
import { connect } from 'react-redux';
import { getAction } from '../actions/actions.js';
import { getActionAll } from '../actions/actions.js';
import { getPopupData } from '../actions/actions.js';
class DataViewContainer extends React.Component {
constructor() {
super();
this.popupCategory = undefined;
this.popupMonth = undefined;
this.state = {
detailPopup : false,
refreshView: false
}
this.handleAddYear = this.handleAddYear.bind(this);
this.handleSubtractYear = this.handleSubtractYear.bind(this);
this.handleGetDetail = this.handleGetDetail.bind(this);
}
componentDidMount() {
this.props.getAction(2016);
this.props.getActionAll(2016);
}
render() {
return (
<div className="row">
<div className="col-sm-8">
<MonthView transactions={this.props.storeData.storeData} selectedYear={this.props.storeData.selectedYear} onAddYear={this.handleAddYear} onSubtractYear={this.handleSubtractYear} onHandleGetDetail={this.handleGetDetail} />
</div>
<div className="col-sm-4">
<MonthViewPopup modalActive={this.state.detailPopup} transactions={this.props.storePopupData.getPopupData} selectedYear={this.props.storeTransactions.selectedYear} category={this.popupCategory} month={this.popupMonth}/>
</div>
</div>
)
}
handleGetDetail(category,month) {
console.log("props inside handleGetDetail: ", this.props);
this.popupCategory = category;
this.popupMonth = month;
let popupYear = this.props.storeTransactions.selectedYear
this.props.getPopupData(popupYear, month, category);
this.setState({ detailPopup: true}, function () {});
}
}
const mapStateToProps = (state) => ({
storeData: state.storeData,
storePopupData: state.storePopupData,
storeDataAll: state.storeDataAll
});
export default connect(mapStateToProps, {getAction,getActionAll,getPopupData})(DataViewContainer);
DataViewPopup component
import React from 'react';
import Modal from 'react-modal';
import { connect } from 'react-redux';
import { saveAction } from '../actions/actions.js';
import { getActionAll } from '../actions/actions.js';
class DataViewPopup extends React.Component {
constructor (props) {
super(props)
this.editedData = new Map();
this.state = {
modalIsOpen: false
};
this.openModal = this.openModal.bind(this);
this.afterOpenModal = this.afterOpenModal.bind(this);
this.closeModal = this.closeModal.bind(this);
this.renderFilteredTransactions = this.renderFilteredTransactions.bind(this);
}
openModal() {
this.setState({modalIsOpen: true});
}
afterOpenModal () {
console.log("inside afterOpenModal");
}
closeModal () {
this.props.saveTransactions(this.editedData);
this.editedData = new Map();
this.setState({ modalIsOpen: false }, function () {});
}
componentWillUnmount() {
this.props.getDataAll(); // i tried this but it does not work because the component (modal popup) is still mounted, but just not visible
//this.props.refreshParent();
}
componentWillReceiveProps(nextProps){
if (nextProps.modalActive === true) {
this.openModal();
return;
}
}
render () {
return <div>
<Modal
isOpen={this.state.modalIsOpen}
//isOpen={this.modalActive}//not needed as is currently setup
onAfterOpen={this.afterOpenModal}
onRequestClose={this.closeModal}
//style={customStyles}
contentLabel="Example Modal"
>
<button onClick={this.closeModal}>close</button>
{this.renderFilteredTransactions(this.props.data,this.props.category,this.props.month)};
</Modal>
</div>
}
renderFilteredTransactions(trans,category,month){
return <div>
<table>
<tbody className="mo-tblBody">
{trans && trans.map((data,index) =>
<tr key={data.transid}>
<td>{data.transid}</td>
<td>
{this.renderCategory(data.category,index,data.transid)}
</td>
</tr>
)}
</tbody>
</table>
</div>
}
handleCategoryChange(value,transIndex,transId){
let trans = JSON.parse(JSON.stringify(this.props.transactions));
//add or updated transaction to this.editedTransactions based on whether or not the transid already exists
trans.filter(item => item.transid === transId)
.map(item => this.editedData.set(transId,
Object.assign({}, item, { category: value })));
}
}
const mapStateToProps = (state) => {
return {storeDataAll: state.storeDataAll,
storeSaveData: state.storeSaveData
}
};
export default connect(mapStateToProps, {getDataAll,saveData})(DataViewPopup);
I covered this kind of question in my recent post Practical Redux, Part 10: Managing Modals and Context Menus.
The basic approaches are:
Pass a callback function as a prop to the modal component (which will work, but if you're driving the modal display from Redux state, putting functions into the Redux state is not encouraged)
Create a "pre-built" action, pass that as a prop to the modal, and have the modal dispatch that action as a "return value" when it closes
Use a middleware like redux-promising-modals to track actions for opening and closing modals, and use the promises it returns to handle "return values" when modals are closed.

Resources