I have a modal that is completely self contained. The modal is opened via going to the modal route and all the functionality to close the modal from button or outside clicks is within the modal component. Basically the modal is not controlled by any parent passing state. I was given a task of making the modals button customizable, meaning passing in a new button component, so we can add the modal to our lib instead of copy pasting the code in projects. Lol this seemed simple enough, and maybe it is and I am just overthinking this.
I cant paste the actual code but I can use a contrived example. This is a very simplified version of the modal, keeping in mind it opens via route so there's really no state and setState in the actual code. Also here is a fiddle
const ModalHeader = ({ onClose }) => {
return (
<div className="modal__header">
<button
className="modal__close-btn"
data-testid="modal-close-button"
onClick={onClose}
/>
</div>
);
};
const Modal = ({ children }) => {
const [state, setState] = React.useState(true);
const handleCloseOutsideClick = () => {
setState(false);
};
const handleCloseButtonClick = () => {
setState(false);
};
const renderModal = () => {
return (
<div className="modal-overlay" onClick={handleCloseOutsideClick}>
<div className="modal">
<ModalHeader onClose={handleCloseButtonClick} />
{children}
</div>
</div>
);
};
return state ? renderModal() : null;
};
const App = () => {
return (
<Modal>
<div>Modal Children</div>
</Modal>
);
};
ReactDOM.render(<App />, document.querySelector('#app'));
I tried a few things, initially I attempted to find a way to pass in a new header component containing a button. Then as I got into the code I realized what I was doing would lose the self contained functionality of the modal. My approach was along the lines of below but obviously the onClick would be an issue since invoking the close functionality is internal.
So I tried using cloneElement to add props within the component if the custom header was detected:
// inside modal component
React.useEffect(() => {
React.Children.map(children, (child: React.ReactElement) => {
if (child && child.type === ModalHeader) {
setHederFound(true);
}
});
}, []);
// inside modal render:
<div className={modalClasses} onClick={stopPropagation}>
{!headerFound ? (
<ModalDefaultHeader onClose={handleCloseButtonClick} />
) : (
React.Children.map(children, (child: React.ReactElement) => {
if (child && child.type === ModalHeader) {
return React.cloneElement(child, {
onClose: handleCloseButtonClick,
});
}
})
)}
{children}
</div>;
Obviously that did not work because there's no onClick in the custom button. Anyways I am thinking that I am over complicating this. I just need a way to pass in a custom button while leaving the functionality internal to the modal. Any assistance would be appreciated.
Thanks in advance.
Related
The problem is...
The first popup renders fine.
But when I try to render the second popup, it's not working.
A new popup is not invoked, the previous popup is refreshed.
I want to call a new popup when I clicked a cell in the grid.
my code is like this
const Main = () => {
const [isPopupOpen, setIsPopupOpen] = useState(false);
return (
<>
... other components (including grid)
{ isPopupOpen && <Popup />}
</>
)
};
when Grid is Clicked, 'isPopupOpen' is updated to true.
I use 'react-new-window' library, and this library use 'window.open()' ((https://github.com/rmariuzzo/react-new-window)
so I set different window names to call several popups.
but I can't solve the problem.
I try to set a state object that has a boolean value.
const [popupObj, setPopupObj] = useState({});
when the grid is clicked, popupObj updates like
{'cellA': true, 'cellD': true}
and a return statement is like
{popupObj[cellName] && <Popup /> }
but the result was the same.
what should I do to solve this problem?
I wrote an example for you. Hope it helps.
use popupIds state to store the popups that you want to open
use Set to toggle the popupIds in the addPopup click handler
import * as React from "react";
export default function App() {
const [popupIds, setPopupIds] = React.useState([]);
const addPopup = (popupId) => {
const set = new Set(popupIds);
if (set.has(popupId)) {
set.delete(popupId);
} else {
set.add(popupId);
}
setPopupIds(Array.from(set));
};
return (
<div className="App">
{["hello", "react"].map((popupId) => (
<div onClick={() => addPopup(popupId)}>{popupId}</div>
))}
{popupIds.map((popupId) => (
<Popup title={getPopupTitle(popupId)} />
))}
</div>
);
}
const getPopupTitle = (popupId) => `title for ${popupId}`;
const Popup = ({ title }) => <div>{title}</div>;
Here is a codesandbox that you can play with directly.
You need to add your popup in an array, so you can render many popup as you want, then you need to define in How much time you will remove a added popup from array or add a close button
Extra: you can configure in global state to access in all your application to your popups and you will have a component like this: https://www.npmjs.com/package/notistack
<OneProfileKeyCard
title="Qualification"
showMoreText="See all qualifications"
onShowMoreClick={() => console.log('show more')}
>
Creating, communicating, and implementing the organization's vision, mission, and overall direction Leading the development and implementation of the overall organization's strategy.
</OneProfileKeyCard>
import React from 'react'
import './OneProfileKeyCard.scss'
type Props = {
title: string
showMoreText: string
onShowMoreClick: () => void
}
export const OneProfileKeyCard: React.FC<Props> = ({
title,
showMoreText,
onShowMoreClick,
children
}) => (
<div className="one-profile-key-card">
<h3>{ title }</h3>
<div>
{ children }
</div>
<button type="button" onClick={onShowMoreClick}>
{ showMoreText }
</button>
</div>
)
could anyone help me to set up a modal? Im trying to set up a modal once onShowMoreClick is clicked that would turn the children(creating, communicating, and implementing the organization...) into a modal. So far it looks like this:
You will need to have a state-managed in the parent component of where the OneProfileKeyCard child component is called from.
Something like this
const Parent = () => {
const [modalOpen, setModalOpen] = React.useState(false)
return (
<div>
<h1>Demo</h1>
<OneProfileKeyCard
title="Qualification"
showMoreText="See all qualifications"
onShowMoreClick={() => setModalOpen(!modalOpen)}>
text ... text
</OneProfileKeyCard>
</div>
)
}
I'm not sure what else is within your components, but you'll then need a way to close the model, right now I have set the showMoreClick prop to open/close, but if that should open then set it to true and do a similar pass-through for a closing false function.
im facing this weird behavior when trying to update the parent component with an set function to the child with props
this hook is to open and close the modal to edit an element
//PARENT FILE
//hook
const [isEditModalOpen, setEditModalOpen] = useState(false)
//more code...
//modal
{isEditModalOpen && <EditExcerciseModal setEditModalOpen={setEditModalOpen} isEditModalOpen={isEditModalOpen} />}
and this is the child code
//CHILD FILE
export const EditExcerciseModal = ({setEditModalOpen, excerciseInfo,fetchExcercisesFromRoutine})
//more code etc etc
<div className="addExcerciseModalContainer">
<span onClick={() =>{ setEditModalOpen(false) }} className="xModal">X</span>
i checked and the onClick is working. if i change the parent state manually the Modal works fine and closes.
the weird case when it its working is when instead of calling the set function i create a function with a setTimeout without time like this:
function closeModal(){
setTimeout(() => { setEditModalOpen(false)}, 0);
}
any ideas?
thanks for the help
You need to create a separation of concern. A Modal consists of three parts
The Modal of its Self.
The Content of the Modal.
And the container of the two.
You should be using the useState() hook and calling setEditModalOpen in the same containing component.
You need to make sure that you're declaring and setting state inside the same component.
// children would be the content of the modal
const Modal = ({ children, selector, open, setOpen }) => {
// we need the useEffect hook so that when we change open to false
// the modal component will re-render and the portal will not be created
useEffect(() => {
setOpen(false);
//provide useEffect hook with clean up.
return () => setOpen(true);
}, [selector]);
return open ? createPortal(children, selector) : null;
};
export const EditExerciseModal = ({ close }) => {
return (
<div>
{/* Instead of creating a handler inside this component we can create it in it's parent element */}
<span onClick={close}>X</span>
{/* Content */}
</div>
);
};
export const ModalBtn = () => {
const [isEditModalOpen, setEditModalOpen] = useState(false);
// this is where it all comes together,
// our button element will keep track of the isEditModalOpen variable,
// which in turn will update both child elements
// when true useEffect hook will re-render Modal Component only now it "will" createPortal()
// when our EditExerciseModal alls close it will set change the isEditModalOpen to false
// which will be passed to the Modal component which
// will then cause the component to re-render and not call createPortal()
return (
<>
<button onClick={() => setEditModalOpen(true)}>EditExerciseModal</button>
{setEditModalOpen && (
<Modal
open={isEditModalOpen}
setOpen={setEditModalOpen}
selector={'#portal'}>
<div className='overlay'>
<EditExerciseModal close={() => setEditModalOpen(false)} />
</div>
</Modal>
)}
</>
);
};
Hi I try to use map data with modals. I set state for all modal but after I use map data array object. I have problems the modals show duplicated . Sorry I think my explanation does't clear so I will show you my code below
for example this is my object
const obj = [
{
id:1
name:"test"
},
{
id:2
name:"test2"
},
{
id:3
name:"test3"
}
]
this is my Modal components ( this just for an example modal code I didn't put all className)
const Modal = ({ children, show = false }) => {
const [showModal, setShowModal] = useState(show);
useEffect(() => {
setShowModal(show);
}, [show]);
return showModal ? (
<div>
<div className="text-xl">title</div>
{children}
</div>
) : null;
};
as you see in my Modal component I send props show for check if it's true show modal else not show
Here is my listModal in this component when I click each card I want modal show each card by id
const ListModal = ({}) => {
const [showEditModal, setShowEditModal] = useState(false);
return obj.map((list) => {
return (
<>
<Card key={list.id} onClick={() => setShowEditModal(true)}>
<Modal show={showEditModal}>{list.id}</Modal>
</Card>
</>
);
});
};
export default ListModal;
the problem is in {list.id} in must return in 1,2,3, but it's all return 1 and I try to inspect element and I set css styled display:none into modal element and I saw {list.id} = 2 that's mean all modal is show but it all stack at same place any idea how to fix this
You should try to use id in setShowEditModal() to match the modal.
I have a test site HERE
Please do:
Visit the site and click on the hamburger icon in the top right. The
side nav should open.
Perform the same action again and you will see the problem I am
currently having.
The side nav does not close properly because I have two conflicting functions operating.
The first conflicting function is an onClick toggle function within the actual hamburger component which toggles an associated context state.
The second conflicting function is used by the side nav panel component. It is bound to a hook which uses its ref to check whether the user has clicked inside or outside of the component.
These functions work up until the user clicks on the hamburger menu whilst the side nav is open. The hamburger is technically outside of the side nav component but overlayed on top of it, so the click does not register as outside. This results in both functions firing one after the other. The "click outside" hook fires and sets the side nav context to false, closing it. The hamburger toggle function then fires, finds the context set to false and changes it back to true. Thus instantly closing and then reopening the side nav.
The solution I have attempted looks like the below. I tried to assign a ref within the hamburger child component and pass it to the side nav parent. I wanted to try and compare the callback returned from useClickedOutside to the toggleRef given to the parent but the hamburger component and then if they match then do nothing. This, I was hoping, would knock one of the functions out of action for that interaction. Not entirely sure this is the best way to attempt to achieve something like this.
Perhaps I could get the side nav bounding box and then check if the click coordinates land within it?
//SideNav.jsx
const SideToggle = ({ sideNavToggle, sideIsOpen, setToggleRef }) => {
useEffect(() => {
const toggleRef = React.createRef();
setToggleRef(toggleRef);
}, []);
return (
<Toggle onClick={sideNavToggle}>
<Bars>
<Bar sideIsOpen={sideIsOpen} />
<Bar sideIsOpen={sideIsOpen} />
<Bar sideIsOpen={sideIsOpen} />
</Bars>
</Toggle>
);
};
export default function SideNav() {
const [toggleRef, setToggleRef] = useState(null);
const { sideIsOpen, sideNavToggle, setSideIsOpen } = useContext(
NavigationContext
);
const handlers = useSwipeable({
onSwipedRight: () => {
sideNavToggle();
},
preventDefaultTouchmoveEvent: true,
trackMouse: true,
});
const sideRef = React.createRef();
useClickedOutside(sideRef, (callback) => {
if (callback) {
if (callback === toggleRef) {
return null;
} else setSideIsOpen(false);
}
});
return (
<>
<SideToggle
sideIsOpen={sideIsOpen}
sideNavToggle={sideNavToggle}
setToggleRef={setToggleRef}
/>
<div ref={sideRef}>
<Side sideIsOpen={sideIsOpen} {...handlers}>
<Section>
<Container>
<Col xs={12}>{/* <Menu /> */}</Col>
</Container>
</Section>
</Side>
</div>
</>
);
}
The useClickedOutside hook used above.
//use-clicked-outside.js
export default function useClickedOutside(ref, callback) {
useEffect(() => {
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
callback(event.target);
} else return null;
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
}
EDIT
Managed to fix part of the problem by moving the toggle component inside of the side nav component. It now has its own problem in that the hamburger component completely rerenders when the side nav props change. Trying to figure out how to stop the hamburger rerendering in a hooks scenario such as this. I've been reading that React.memo may be the answer but unsure how to implement it as of yet.
//SideNav.jsx
export default function SideNav() {
const { sideIsOpen, sideNavClose, sideNavOpen } = useContext(
NavigationContext
);
const handlers = useSwipeable({
onSwipedRight: () => {
sideNavClose();
},
preventDefaultTouchmoveEvent: true,
trackMouse: true,
});
const sideRef = React.createRef();
useClickedOutside(sideRef, (callback) => {
if (callback) {
sideNavClose();
}
});
const SideToggle = () => {
const handleClick = () => {
if (!sideIsOpen) {
sideNavOpen();
}
};
return (
<Toggle onClick={() => handleClick()}>
<Bars>
<Bar sideIsOpen={sideIsOpen} />
<Bar sideIsOpen={sideIsOpen} />
<Bar sideIsOpen={sideIsOpen} />
</Bars>
</Toggle>
);
};
return (
<>
<SideToggle />
<div ref={sideRef}>
<Side sideIsOpen={sideIsOpen} {...handlers}>
<Section>
<Container>
<Col xs={12}>{/* <Menu /> */}</Col>
</Container>
</Section>
</Side>
</div>
</>
);
}
I'd recommend using this package: https://github.com/airbnb/react-outside-click-handler . It handles some edge cases you mention, you can check their source code if you are interested in the details.
Then you need to keep the information about whether the sidebar is opened in the state and pass it down to the affected components somewhat like this (this way you can also change how the site looks in other places depending on the toolbar state, eg. disable scrollbar etc):
function Site() {
const [isOpened, setToolbarState] = React.useState(false);
return (
<React.Fragment>
<Toolbar isOpened={isOpened} setToolbarState={setToolbarState} />
{isOpened ? (<div>draw some background if needed</div>) : null}
<RestOfTheSite />
</React.Fragment>
);
}
function Toolbar(props) {
const setIsClosed = React.useCallback(function () {
props.setToolbarState(false);
}, [props.setToolbarState]);
const setIsOpened = React.useCallback(function () {
props.setToolbarState(true);
}, [props.setToolbarState]);
return (
<OutsideClickHandler onOutsideClick={setIsClosed}>
<div className="toolbar">
<button onClick={setIsOpened}>open</button>
...
{props.isOpened ? (<div>render links, etc here</div>) : null}
</div>
</OutsideClickHandler>
);
}
This way you won't necessary need to juggle refs and handlers around too much.
The way I answered this was to avoid detecting a click outside of the side nav altogether and instead detected if the click was outside of the main body of the site.
If true, then I call the navigation context and set side nav to false the same as I was trying in the side nav itself.