Im trying to create a react Modal component from scratch. I would like to add the functionality of closing the modal when another one is opened.
I know the logic how to solve(i think i know), but cant implement it. My approach would be using context, where i store the current modal opened(currentModal) and if another one is opened then it would check if there is a currentModal and if so it would close it.
So far i have the modal component:
export function Modal({title, isOpen, children, onClose}){
return(
createPortal(
<trds-modal class={isOpen ? 'opened': ''} onClick={onClose}>
<trds-modal_container onClick={e => e.stopPropagation()}>
<trds-modal_header>
<h2>{title}</h2>
<Icon icon="x" onClick={onClose} />
</trds-modal_header>
<trds-modal_body>
{children}
</trds-modal_body>
</trds-modal_container>
</trds-modal>, document.body)
)
}
i figured it out.
Created a context provider where i store the id of the current modal opened.
export function ModalContextProvider({children}){
const [currentModalId, setCurrentModalId] = useState(null);
return(
<modalContext.Provider value={[currentModalId, setCurrentModalId]}>
{children}
</modalContext.Provider>
)
}
then in the modal component i generate a uniqe id and set the context's currentModalId to that. And if the currentModalId changes then the modal checks if that equals to the modalId. If not, it calls the onClose function.
export function Modal({title, isOpen, children, onClose}){
const modalId = useMemo(() => generateId(), []);
const [currentModalId, setCurrentModalId] = useContext(modalContext);
useEffect(() => {
if(isOpen){
setCurrentModalId(modalId);
}
}, [isOpen, setCurrentModalId, modalId]);
useEffect(() => {
if(currentModalId !== modalId) onClose();
}, [currentModalId, modalId, onClose]);
return(
createPortal(
<trds-modal class={isOpen ? 'opened': ''} onClick={onClose}>
<trds-modal_container onClick={e => e.stopPropagation()}>
<trds-modal_header>
<h2>{title}</h2>
<Icon icon="x" onClick={onClose} />
</trds-modal_header>
<trds-modal_body>
{children}
</trds-modal_body>
</trds-modal_container>
</trds-modal>, document.body)
)
}
I hope it help you as i couldn't find an approach to this problem. (maybe its not even an existing problem :D)
Related
I got a OnClick which actually receives an id:
<Button onClick={() => addToCart(id)} >Buy</Button>
On the other hand, in a different JS file,I got a modal which appears with a click via useState:
const [stateModal1, changeModalState1] = useState(false);
Now, in the same component I work with this modal, I map an array which returns a Button, which now is working with the "addToCart(id)" value mentioned before, like this:
{products.map((product) => {
return <Product image={product.image}
key={product.id}
data={product}
addToCart={() =>addToCart(product.id)} />})}
The question that is driving me crazy is: how can I use the button in the mapped array to trigger that modal, and at the same time, to pass values to that modal in order to show the mapped item IN that modal?
Thanks in advance.
EDIT: this is the modal, which is another component:
const Modal = ({
children,
state,
stateModal1,
})
return (
<>
{state &&
<Overlay>
<Container>
<CloseButton onClick={() => changeState(false)}>{FaWindowClose}</CloseButton >
{children}
<Header>
<h3>Confirm buy</h3>
<h4>{name}</h4>
<h4>$ {price}</h4>
</Header>
<Button onClick={() => changeState(false)}>Confirm</Button>
</Container>
</Overlay>
}
</>)
PS: the "confirm" button which triggers the "changeState()", should also trigger the addToCart().
As mentioned by other comments above, you can pass a prop to the modal component from the parent component to achieve your demand normally.
The only thing that needs to be done is set the open/close modal state and the passing data state at the same time, or, probably use one state directly
sample of the code:
import "./styles.css";
import "antd/dist/antd.css";
import { useState } from "react";
import { Modal } from "antd";
export default function App() {
// init with undefined, if not undefined, open the modal
const [modal, setModal] = useState(undefined);
const list = [...Array(20).keys()];
// set the state to open the modal, as well as pass it to the modal itself as a prop if necessary
const handleClick = (idx) => () => {
setModal(idx);
};
return (
<div className="App">
{list.map((x, idx) => (
<div style={{ border: "1px solid black" }} onClick={handleClick(idx)}>
{x}
</div>
))}
<Modal visible={modal !== undefined}>The value you passed: {modal}</Modal>
</div>
);
}
the online demo could be found here: https://codesandbox.io/s/hardcore-shape-89y78?file=/src/App.js
I'm building a page that will render a dynamic number of expandable rows based on data from a query.
Each expandable row contains a grid as well as a button which should add a new row to said grid.
The button needs to access and update the state of the grid.
My problem is that I don't see any way to do this from the onClick handler of a button.
Additionally, you'll see the ExpandableRow component is cloning the children (button and grid) defined in SomePage, which further complicates my issue.
Can anyone suggest a workaround that might help me accomplish my goal?
const SomePage = (props) => {
return (
<>
<MyPageComponent>
<ExpandableRowsComponent>
<button onClick={(e) => { /* Need to access MyGrid state */ }} />
Add Row
</button>
<MyGrid>
<GridColumn field="somefield" />
</MyGrid>
</ExpandableRowsComponent>
</MyPageComponent>
</>
);
};
const ExpandableRowsComponent = (props) => {
const data = [{ id: 1 }, { id: 2 }, { id: 3 }];
return (
<>
{data.map((dataItem) => (
<ExpandableRow id={dataItem.id} />
))}
</>
);
};
const ExpandableRow = (props) => {
const [expanded, setExpanded] = useState(false);
return (
<div className="row-item">
<div className="row-item-header">
<img
className="collapse-icon"
onClick={() => setExpanded(!expanded)}
/>
</div>
{expanded && (
<div className="row-item-content">
{React.Children.map(props.children, (child => cloneElement(child, { id: props.id })))}
</div>
)}
</div>
);
};
There are two main ways to achieve this
Hoist the state to common ancestors
Using ref (sibling communication based on this tweet)
const SomePage = (props) => {
const ref = useRef({})
return (
<>
<MyPageComponent>
<ExpandableRowsComponent>
<button onClick={(e) => { console.log(ref.current.state) }} />
Add Row
</button>
<MyGrid ref={ref}>
<GridColumn field="somefield" />
</MyGrid>
</ExpandableRowsComponent>
</MyPageComponent>
</>
);
};
Steps required for seconds step if you want to not only access state but also update state
You must define a forwardRef component
Update ref in useEffect or pass your API object via useImerativeHandle
You can also use or get inspired by react-aptor.
⭐ If you are only concerned about the UI part (the placement of button element)
Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.
(Mentioned point by #Sanira Nimantha)
In some components i am using recoil atoms to manage my states. One example is my modal component. It look something like this:
export const modalState = atom({
key: "modalState",
default: false
})
export const useToggleModalState = () => {
const setModalState = useSetRecoilState(modalState)
return (state, callback) => {
setModalState(state)
if (callback) {
callback()
}
}
}
export const Modal = (props) => {
<Transition show={modalState}>
<Dialog>
<Dialog.Title>My Modal Headline</Dialog.title>
<Dialog.Description>My Modal Description</Dialog.Description>
</Dialog>
</Transition>
}
and i am using this modal like this:
const toggleModalState = useToggleModalState();
return (
<Modal />
<Button text="Close Modal" onClick={() => toggleModalState(false)} />
)
however, if I use the modal multiple times, the modal is automatically duplicated, but I still use only one state for all modals. Of course, I don't want that. If I use a component multiple times, I want the state to be created multiple times, so that I can change the state of each component individually.
I have read that there are also atomFamilys. Could I use these at this point? What should my code look like then? Can multiple atoms also be created automatically if I use a component multiple times?
Why do you want to use recoil for that? The state of the modal is tied to the modal itself, it doesn't need to access global state.
You can just use useState to determine if you want to show a modal within a component:
export const Modal = (props) => {
<Transition show={props.show}>
<Dialog>
<Dialog.Title>My Modal Headline</Dialog.title>
<Dialog.Description>My Modal Description</Dialog.Description>
</Dialog>
</Transition>
}
export const ComponentWithModal = () => {
const [showModal, setShowModal] = useState(false);
return (
<>
<Modal show={showModal}/>
<Button text="Open Modal" onClick={() => setShowModal(true)} />
<Button text="Close Modal" onClick={() => setShowModal(false)} />
</>
)
}
I've been looking through related posts and tried a few solutions but I wasn't able to adapt any of them to my situation.
I am trying to change the state of selectedPin by sending the function and calling it from the child component.
See below my code
Parent component :
export default function Pins(data) {
const chargers = data.data;
const [selectedPin, setSelectedPin] = useState(null);
const handleClose = () => {
setSelectedPin(null);
};
return (
<>
{chargers.map((charger, index) => (
<Marker
key={index}
longitude={charger.AddressInfo.Longitude}
latitude={charger.AddressInfo.Latitude}
>
<Button
onClick={(e) => {
e.preventDefault();
setSelectedPin(charger);
}}
>
<img src="./assets/pin.svg" alt="marker pin" />
</Button>
</Marker>
))}
{selectedPin ? (
<PopupInfo selectedPin={selectedPin} handleClose={handleClose} />
) : null}
</>
);
}
Child component :
export default function PopupInfo(selectedPin, { handleClose }) {
const pin = selectedPin.selectedPin;
console.log(pin);
console.log(handleClose);
return (
<Popup
latitude={pin.AddressInfo.Latitude}
longitude={pin.AddressInfo.Longitude}
onClose={handleClose}
>
<div>Popup Info</div>
</Popup>
);
}
When console logging handleClose in child component I am getting undefined.
I think you should pass them both down as named props:
export default function PopupInfo({ selectedPin, handleClose })
or do this:
export default function PopupInfo(props) and then use props.selectedPin and props.handleClose.
It won't work as React is Uni-directional data flow model - it'a core feature of React.
State of Parent can't be modified by Child. You have to come-up with an idea to handle the data in Parent itself or pass that as prop to bypass(not recommended)
If you are interested to know more : https://www.youtube.com/watch?v=hO8u07-WTOk
How to call a Material UI Dialog during onClick on delete icon ie onClick={deletePlayer(id)} ?
I have added the Dialog.js under modal/Dialog and imported to Home component.
I have added a demo here
Short answer: Forked CodeSandbox with working dialog
Long answer:
First of all, you need to move the display/dismiss logic out of the AlertDialog component and into the component that actually triggers the display of the modal (in your case, the Home component). This means that you'll receive the open state and onClose handler as props (along with the playerId which will hold the ID of the player being targeted for deletion). So the signature of your dialog component becomes:
export default function AlertDialog({ open, onClose, playerId }) {
return (
<Dialog open={open} onClose={onClose} ...> ... </Dialog>
);
}
In Home, we add the logic to track and set the state of both the dialog open/closed status, and the ID of the player targeted for deletion. We do this through useState:
const [deleteDialog, setDeleteDialog] = useState(false);
const [playerId, setPlayerId] = useState("");
While you could have as many AlertDialog components as you have players by adding <AlertDialog /> inside your player map loop, it is redundant as you'll only ever have one modal active (by definition). So all you have to do is place a single instance of <AlertDialog /> in your Home component. A good convention is to place it before the closing encompassing tag:
return (
<div className="App">
.
.
.
<AlertDialog
open={deleteDialog}
onClose={() => setDeleteDialog(false)}
playerId={playerId}
/>
</div>
);
Finally, we deal with the handler responsible for displaying the modal, in your case deletePlayer. We have two things to do there: set the player ID targeted for deletion through the playerId state variable, and display the modal through the deleteDialog state variable:
const deletePlayer = id => e => {
setPlayerId(id);
setDeleteDialog(true);
};
Create a state in the Home component to handle the Dialog visibility, set the state on click and render the AlertDialog conditionally:
const [openDialog, setOpenDialog] = useState(false);
...
const deletePlayer = id => e => {
setOpenDialog(true);
};
...
return(
...
{openDialog && (
<AlertDialog isOpen={openDialog} setIsOpen={setOpenDialog} />
)}
Then in the AlertDialog component:
export default function AlertDialog(props) {
const { isOpen, setIsOpen } = props;
const handleClose = () => {
setIsOpen(false);
};
return (
<div>
<Dialog
open={isOpen}
onClose={handleClose}
...
Working Example: