React - Manage Bootstrap Modals in components - reactjs

I have a ReactJS app with 4 screens/components. Each screen can link to another one.
I want to use Modals to display content of each screen, this way I don't lose the state of the current screen.
For now I just set the Modal on my 1st component :
<Modal show={this.state.show}
ref={this.ModalGlobal}
onHide={() => this.setState({show: false})}
>
<Modal.Body>
{this.state.id &&
<MyComponentB id={this.state.id} />
}
</Modal.Body>
</Modal>
On my ComponentB, I want to open the same Modal with different ID.
I tried to use references, but I don't know what to do with that in my ComponentB ?
Like :
this.ModalGlobal.current.destroy
Do I have to use Redux or can it be done using contexts or other solution ?

Instead of having one modal close another one and open that one, would it be possible to instead have the modal update its own contents based on the ID? You could make a wrapper for the modal that will update the body of the modal depending on the current ID. Something like this:
const MyModal = ({id}) => {
const [modalPage, setModalPage] = useState(id);
const [modalIsOpen, setModalIsOpen] = useState(false);
useEffect(() => {
setModalPage(id)
}, [id]);
const openModal = async () => {
setModalIsOpen(true);
document.body.style.overflowY = "hidden";
}
const closeModal = () => {
setModalIsOpen(false);
document.body.style.overflowY = "";
}
const modalPages = {
'welcome': <WelcomeComponent setModalPage />,
'products': <ProductsComponent setModalPage />,
'contact': <ContactComponent setModalPage />
}
const content = modalPages[modalPage];
return (
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
className="react-modal"
overlayClassName="react-modal-overlay"
>
{content}
</Modal>
);
}

Related

Test modal component within another component

I'm trying to test a component that should open its modal. Modal is a part of this component, but it's rendered with createPortal(). I first check if modal exist in the document and after button click if it appeared but test fails.
Component:
const [openModal, setOpenModal] = useState(false);
function Component() {
return (
<div>
<button onClick={() => setOpenModal(true)}>Open Modal</button>
<Modal open={openModal}/>
</div>
)
}
Modal:
const Modal = ({ open, children }) => {
return createPortal(
<div style={{display: open ? "block" : "none"}} data-testid="modal">
{children}
</div>,
document.getElementById("modals")
);
};
Test:
test("component that opens modal", async () => {
render(<Component />);
const button = screen.getByText("Open Modal");
const modal = screen.queryByTestId("modal");
expect(modal).not.toBeInTheDocument();
fireEvent.click(button);
await waitFor(() => expect(modal).toBeInTheDocument()); // Fails
});
I tried to test it with await waitFor(() => expect(modal).toBeInTheDocument()) and also with standard expect(modal).toBeInTheDocument()). I also tried to render modal without portal, but still had no effect on the test. Could you please explain how it should be tested?
This kind of behavior is probably generating a new render, try using act
Some useful links: https://github.com/threepointone/react-act-examples/blob/master/sync.md
https://testing-library.com/docs/preact-testing-library/api/#act

How to trigger close modal in Chakra UI?

I am using Modal Component in Chakra UI to show Input editor post (Input editor is a children component wrapped by Modal component). I want to trigger close modal from Input editor component if data was fetched as succeed.
Here's my component for Modal:
//...import component from Chakra Modal
import {Post as PostModal} from '../components/Modal'
export const Post = () => {
const { isOpen, onOpen, onClose } = useDisclosure()
return (
<div>
<Modal onClose={onClose} size='full' isOpen={isOpen} trapFocus={false} >
<ModalOverlay />
<ModalContent>
<ModalHeader>Add Post</ModalHeader>
<ModalCloseButton />
<ModalBody>
<div>
<PostModal />
</div>
</ModalBody>
</ModalContent>
</Modal>
</div>
)
}
And my code handle fetcher post data:
/components/Modal
const fetcher = async (data) => {
const _ = await sendPost(data);
if (_) {
//here i want to trigger close modal
}
}
const PostInput = () => {
return (
//<Input/>
//<Textarea> etc...
)
}
Who can help me please?
You can call the onClose function once the API call is successful , as:
const fetcher = async (data) => {
let res = await sendPost(data);
if (res.data) {
onClose()
}
}

How to update back prop to child componet using react hook

I have a parent componet like this, just to show the dialog
The Child Component ( Main to show dialog)
export const MedicalRecord = memo(function MedicalRecord() {
// const onPressViewAll = useCallback(() => {}, [])
const [show, setShow] = useState(false) ///to show dialog
function hanndleDialog() {
setShow(!show) set to show dialog
}
// useEffect(() => {
// if (show == true) {
// setShow(!show)
// }
// },[show])
return (
<SummaryViewContainer
count={5}
title={"dashboardScreen.medicalRecords.title"}
onPress={() => {
hanndleDialog()
}}
>
<View>
{show && (
<ProgressDialog
show={show} //pass t
callback={() => {
hanndleDialog()
}}
/>
)}
<RecordItem />
<RecordItem />
<RecordItem />
</View>
</SummaryViewContainer>
)
})
And parent componet to show this dialog
export default function DialogTesting(show: boolean, { callback }) {
const [showDialog, doShow] = useState(show) //show to present show in child
return (
<View>
{/* <Button
title="click"
onPress={() => {
setShow(true)
}}
>
<Text>Show dialog</Text>
</Button> */}
<Dialog
visible={showDialog}
title="Record New Progress"
style={DIALOG}
onClose={() => {
doShow(false)
callback()
}}
>
But i cant figure out how to open dialog again when close the dialog, it only open for once, i try React Hook : Send data from child to parent component but not work !
How can i show dialog and when i click close button, the children return orignal state so i can click it again, thank you guy so much
Here is a short video of this problem
https://recordit.co/0yOaiwCJvL
I am assuming that you want to find a way to show hide a component based on click. So this is the sandbox for the same.
In this solution, instead of using a derived state, the state is held in the parent's state and the child is mounted/unmounted based on that state.
The state can be updated by a method present in the parent and this method is passed to the child to be triggered on the "hide child" button. The same method is used to show the child component as well.
Below is the core code for the same,
import React from "react";
const Dialog = ({ hideMe }) => {
return (
<div>
<div>I am dialog</div>
<button onClick={hideMe}>Hide me</button>
</div>
);
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = { showDialog: false };
}
toggleDialog = () => {
this.setState((prevState) => {
return { showDialog: !prevState.showDialog };
});
};
render() {
return (
<div>
<div>I am parent.</div>
<button onClick={this.toggleDialog}>Toggle Dialog</button>
{this.state.showDialog ? <Dialog hideMe={this.toggleDialog} /> : null}
</div>
);
}
}
export default App;

node.current is not a function

I am trying to create a function close modal when click outside but I am keep getting this error:
TypeError: node.current is not a function
Here is my following code in MemberCard.js:
const [modalStatus, setModalStatus] = useState(false);
const node = useRef(null);
const openModal = () => {
setModalStatus(!modalStatus);
};
const handleClick = (e) => {
if (node.current(e.target)) {
return;
}
// outside click
setModalStatus(false);
};
useEffect(() => {
document.addEventListener("mousedown", handleClick);
return () => {
document.removeEventListener("mousedown", handleClick);
};
}, []);
return (
<div className="member-card">
<div className="member-edit" onClick={openModal}>
<Symlink />
</div>
{modalStatus && (
<TeamStatusModal
active={modalStatus}
ref={node}
tab={tab}
member={member}
/>
)}
...
}
Here is my modal that I open after click:
const TeamStatusModal = (props) => {
const { active, tab, member, ref } = props;
console.log(ref);
return (
<div
className={`team-status-modal-container ${active ? "ACTIVE_CLASS" : ""}`}
>
<button className="status">
<ProfileIcon /> <span>View Profile</span>
</button>
<hr />
<button className="status">
<MessageIcon /> <span>Message Me</span>
</button>
</div>
);
};
How can I implement this feature?
In react, there are some good libraries that can help you with modals, one of them is called react-modal, you can give it a check.
If you want to implement a modal by yourself, we can follow some steps.
First we need to define a context, because the modal state needs to be accesed by more than one component or page in your app.
In the context, you could store the modal in a isModalOpen state, and add functions to manipulate it, such as openModal and closeModal. It really depends on the amount of features you want to add to this implementation.
Finally, you make the context globally accessible wrapping your app around a provider.
an example implementation
const ModalContext = createContext({})
export const ModalContextProvider = ({children}) => {
const [isModalOpen, setIsModalOpen] = useState(false)
const toggleModalState = () => {
setIsModalOpen(state => !state)
}
return <ModalContext.Provider value={{isModalOpen, toggleModalState}}>{children}<ModalContext.Provider>
}
export const useModal = () => {
return useContext(ModalContext)
}
Now the modal will be available globally

how do control the state for multiple component with one function

I have one simple app that include 3 identical button and when I click the button, onClick event should trigger to display one span. for now, I have use one one state to control span show or not and once I click any one of button all span show. How can I implement the code, so when I click the button, only the correspond span display
import "./styles.css";
import React, { useState } from "react";
const Popup = (props) => {
return <span {...props}>xxx</span>;
};
export default function App() {
const [isOpen, setIsOpen] = useState(true);
const handleOnClick = () => {
setIsOpen(!isOpen);
};
return (
<div className="App">
<button onClick={handleOnClick}> Show popup1</button>
<Popup hidden={isOpen} />
<button onClick={handleOnClick}> Show popup2</button>
<Popup hidden={isOpen} />
<button onClick={handleOnClick}> Show popup3</button>
<Popup hidden={isOpen} />
</div>
);
}
codesandbox:
https://codesandbox.io/s/cocky-fermi-je8lr?file=/src/App.tsx
You should rethink how the components are used.
Since there is a repeating logic and interface, it should be separated to a different component.
const Popup = (props) => {
return <span {...props}>xxx</span>;
};
interface Props {
buttonText: string
popupProps?: any
}
const PopupFC: React.FC<Props> = (props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(!isOpen)}>{props.buttonText}</button>
<Popup hidden={isOpen} {...props.popupProps} />
</>
)
}
export default function App() {
const [isOpen, setIsOpen] = useState(true);
const handleOnClick = () => {
setIsOpen(!isOpen);
};
return (
<div className="App">
<PopupFC buttonText="Show popup1" />
<PopupFC buttonText="Show popup2" />
<PopupFC buttonText="Show popup3" />
</div>
);
}
If each Popup needs its own isOpen state, it would not be possible to achieve with a single boolean state.
Perhaps converting both the button and the span to a single component and letting each Popup component handle its own isOpen:
import "./styles.css";
import React, { useState } from "react";
const Popup = (props) => {
const [isOpen, setIsOpen] = useState(true);
const handleOnClick = () => {
setIsOpen(!isOpen);
};
return (
<>
<button onClick={handleOnClick}>{props.children}</button>
{isOpen && <span {...props}>xxx</span>}
</>
);
};
export default function App() {
return (
<div className="App">
<Popup>Show popup 1</Popup>
<Popup>Show popup 2</Popup>
<Popup>Show popup 3</Popup>
</div>
);
}
That happens simply because you are using the same state "isOpen" for all buttons,
once you click any one of them it reflects all buttons because it's the same value.
you could solve this using Custom Hook since you repeat the logic or you could separate them into small components
Based on your comment, you only want one popup to be open at a time. That was not clear in your original question so the other answers don't address this.
Right now you are just storing a value of isOpen that is true or false. That is not enough information. How do you know which popup is open?
If you want to show just one at a time, you can instead store the number or name (any sort of unique id) for the popup which is currently open.
We make the Popup a "controlled component" where instead of managing its own internal isOpen state, it receives and updates that information via props.
The App component is responsible for managing which popup is open and passing the right props to each Popup component. Since we are doing the same thing for multiple popups, I moved that logic into a renderPopup helper function.
Popup
interface PopupProps {
isOpen: boolean;
open: () => void;
close: () => void;
label: string;
}
const Popup = ({ isOpen, open, close, label }: PopupProps) => {
return (
<>
<button onClick={open}> Show {label}</button>
{isOpen && (
<div>
<h1>{label}</h1>
<span>xxx</span>
<button onClick={close}>Close</button>
</div>
)}
</>
);
};
App
export default function App() {
// store the label of the popup which is open,
// or `null` if all are closed
const [openId, setOpenId] = useState<string | null>(null);
const renderPopup = (label: string) => {
return (
<Popup
label={label}
isOpen={openId === label} // check if this popup is the one that's open
open={() => setOpenId(label)} // open by setting the `openId` to this label
close={() => setOpenId(null)} // calling `close` closes all
/>
);
};
return (
<div className="App">
{renderPopup("Popup 1")}
{renderPopup("Popup 2")}
{renderPopup("Popup 3")}
</div>
);
}
Code Sandbox

Resources