Building a Modal component in React without nesting it inside caller component - javascript

I'm trying to call a Modal from non-related component (without using any parent-child relationship).
In order to do that I'm trying to use React Redux (as the only way I've seen that can make a connection between two unrelated components). An example on CodeSandbox shows the bare minimum of what I'm trying to do.
My issue is that I don't want to include <Modal> inside the <Button> render function. I want to be able to simply flip the flag in Button.js and <Modal> would appear. This is, from what I understand, is suppose to be one of the advantages of Redux.
It may be look unimportant, but besides the fact that I understand that this is something that can be done and so I want to know how, it will be useful for me in a different piece of code in which if I include <Modal> in the component's render function it'll render the Modal multiple times (I render that component in a list).
Edit:
Just to be clear (as per the example on CodeSandbox), I'm using React classes and not functional components; so no hooks like useDispatch but rather functions like mapDispatchToProps are the way I want to go here.

I will Recommend using React Portal, this will inject it inside the given node, I found it to be the best solution for creating modals. I used same in dailylivedeals.com as a POC
import ReactDOM from "react-dom";
render() {
return ReactDOM.createPortal(
this.props.children,
Document.body
);
}
This is the simplest and cleanest using React's own feature.
Advantage:
Cleaner and simpler
Each modal instance can have its own modal
Multiple modals can be opened ( even from inside a modal)
Modal target can be dynamic (like modal inside modal)
Multiple modal can be controlled using code easily.
Update :
Eloborate code for modal
import React, {useEffect, useState} from "react";
import ReactDOM from "react-dom";
import {Link} from 'react-router-dom';
import "./modal.scss";
let Modal = ({visible, id, hideModal, children, ...props}) => {
let [show, setShow] = useState(false);
useEffect(() => {
setShow(visible);
console.log(visible);
}, [visible]);
let toggleVisibility = () => {
//hideModal();
setShow(!show);
}
useEffect(() => {
if (!show) {
hideModal();
}
}, [show]);
return <div className="modal-scratchpad">
{show ?
ReactDOM.createPortal(
<div id={`${id}-modal-wrapper`} className="sample-modal-wrapper">
<div id={`${id}-modal-backdrop`} className="sample-modal-backdrop">
</div>
<div id={`${id}-modal-container`} className="sample-modal-container">
<div id={`${id}-modal`} className="sample-modal">
{children}
<div onClick={toggleVisibility} className="sample-modal-cross-button">{'\u2716'}</div>
</div>
<style type="text/css">
{"body {" +
"overflow:hidden" +
"}"}
</style>
</div>
</div>
, document.body)
: <></>
}
</div>
};
export default Modal;

Related

How to capture paste event anywhere on page in React?

Having looked at other stack overflow questions and on Google, I haven't been able to find an answer to this.
Background:
I'm currently re-writing a jquery application in React. Previously I was just adding a paste event listener to the body of the page. That seemed to allow me to capture any paste event the user would do.
Questions:
In React, with the code below in my App.js file, on initial load of the page it doesn't allow me to capture paste events. Only after clicking somewhere on the page does it then work when I paste. I realise I could just have an input and an onPaste attribute, but I need to be able to capture automatically.
In terms of passing the pasted text down the component tree, with the ability to edit the text from a lower component, am I right in thinking it's best to pass a callback as a prop down from the top-level component so the state is always updated using a function in the top-level component?
Currently this is what I have:
// App.js
import TopBar from "./components/TopBar/TopBar";
import Wrapper from "./components/Wrapper/Wrapper";
import AppContainer from "./components/AppContainer/AppContainer";
function App() {
const [inputText, setInputText] = useState(false);
return (
<div
onPaste={(e) => setInputText(e.clipboardData.getData("Text"))}
className="App"
>
<TopBar />
<Wrapper>
<h2>New Flashcard</h2>
<AppContainer inputText={inputText} setInputText={setInputText} />
</Wrapper>
</div>
);
}
export default App;
Your listener will only work when the specific div is focused, therefore it works on the second attempt. Instead, add the listener to body element or window object:
function Component() {
const [inputText, setInputText] = useState(false);
useEffect(() => {
window.addEventListener("paste", (e) => {
setInputText(e.clipboardData.getData("text"));
});
return () => {
window.removeEventListener("paste");
};
}, []);
return <div className="App">{inputText}</div>;
}
am I right in thinking it's best to pass a callback
No, it depends on the depth, please read about Context API and research on an anti-pattern called "props-drilling".

React component not re rendering on window.location change

So, I have a react component for a main menu.
Its a list of react-router <Link/> which point to different pages. Each link checks the window.location.path to figure out if it is the active page so that the active menu item can render differently.
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { connect } from "react-redux";
import List from "#material-ui/core/List";
class MainMenu extends Component {
render() {
return (
<List>
<Link to="/products" className={window.location.pathname === "/products" ? "active" : null }>Products</Link>
<Link to="/sales" className={window.location.pathname === "/sales" ? "active" : null }>Sales</Link>
</List>
)
}
}
This was actually working fine, however I now want to connect this menu component to my redux store so that I can display some data from the store inside the menu, as soon as I add the connect function the menu breaks such that when changing pages the "active" class doesn't get applied.
i.e. the component does not update even though window.location.pathname has changed.
I haven't included my store code because I've tested it with dummy reducers that do nothing and also I haven't even used the data anywhere in the component yet.
export default connect(state => ({ foo: state.bar }))(MainMenu);
I'm still not sure whether this issue is to do with connect or if connect has just highlighted a problem with using window.location.pathname and react doesn't know when to update.
Any help is greatly appreciated!
React component won't rerender when you change window.location.pathname. Only state changes will cause component to rerender. You need to listen to window.pathname change and map it to state then only your component will rerender
For such things you can take a look at react-router. Add it your project and such things can be easily achieved.
https://reacttraining.com/react-router/core
The view changes when there's any change in this.state of the
component. I would suggest you to listen to changes in window.location
Here's the code
constructor(){
this.state = {currentPath:window.location.pathname}
window.onbeforeunload = function(e) {
this.setState({currentPath:window.location.pathname})
};
}
and in render()
render() {
return (
<List>
<Link to="/products" className={this.state.currentPath === "/products" ? "active" : null }>Products</Link>
<Link to="/sales" className={this.state.currentPath === "/sales" ? "active" : null }>Sales</Link>
</List>
)
}

"Warning: react-modal: App element is not defined. Please use `Modal.setAppElement(el)` or set `appElement={el}`"

How do I fix this warning in console of a React app using the react-modal package:
Warning: react-modal: App element is not defined. Please use Modal.setAppElement(el) or set appElement={el}
I have not been successful at figuring out what el is supposed to be.
Context:
in my App.js root component file:
...
import Modal from 'react-modal';
...
class App extends Component {
...
render(){
...
<Modal
className="modal"
overlayClassName="overlay"
isOpen={foodModalOpen}
onRequestClose={this.closeFoodModal}
contentLabel="Modal"
>
...
}
}
Where ... indicates code not shown.
Everything works fine, but when the Modal is opened, the following Warning appears in my console:
index.js:2177 Warning: react-modal: App element is not defined. Please use Modal.setAppElement(el) or set appElement={el}. This is needed so screen readers don't see main content when modal is opened. It is not recommended, but you can opt-out by setting ariaHideApp={false}.
In the react-modal docs all I can find is the following:
App Element
The app element allows you to specify the portion of your app that should be hidden (via aria-hidden) to prevent assistive technologies such as screenreaders from reading content outside of the content of your modal.
If you are doing server-side rendering, you should use this property.
It can be specified in the following ways:
DOMElement
Modal.setAppElement(appElement);
query selector - uses the first element found if you pass in a class.
Modal.setAppElement('#your-app-element');
Unfortunately, this has not helped! I cannot figure out what el is supposed to represent.
Here are some of the many property variations I have tried adding to my Modal component:
`appElement={el}`,
`appElement="root"` where `root` is the id that my App component is injected into
`appElement={'root'}`
`appElement="div"`,
`appElement={<div>}`,
`appElement={"div"}`
I've also tried calling Modal.setAppElement('root'); from inside src/index.js, where root is the root element that my App component is injected into, and index.js is where I do that.
Add ariaHideApp={false} to Modal attributes.
This should work:
<Modal isOpen={!!props.selectedOption}
onRequestClose={props.clearSelectedOption}
ariaHideApp={false}
contentLabel="Selected Option"
>
</Modal>
Some solutions are given in react-modal issue #133:
The problem lies here:
Depending on when it evaluates react-modal#1.6.5:/lib/helpers/ariaAppHider.js#L1:
document.body does not exist yet and it will resolve to undefined || null.
if Modal.setAppElement() is called with null or not called at all with the <script /> placed on <head /> (same as above).
Probably it can also happen if called with a selector that does not match any results.
Solutions:
Browser Rendering:
#yachaka snippet prevents this behavior by defining the element before placing the <Modal />:
componentWillMount() {
Modal.setAppElement('body');
}
#ungoldman answer, if you don't want to depend on `setAppElement':
Inject the bundled application JS into <body> instead of <head>.
Though ideally react-modal should wait until the DOM is loaded to try attaching to document.body.
server-side:
If rendering on server-side, you must provide a document.body, before requiring the modal script (perhaps it should be preferable to use setAppElement() in this case).
Update:
react docs have been updated to include the information above, so they should now be clearer for users running into this issue.
react-modal issue #567: add information (from issue #133 linked above) to the docs.
Just include appElement={document.getElementById('app')} inside your modal like this
<Modal
className="modal"
appElement={document.getElementById('app')}
>
It will work 100% if app is your central in index.html from where react loads.
This is my TypeScript Modal component which wraps react-modal v3.8.1:
import React from 'react'
import ReactModal from 'react-modal'
interface Props {
isOpen: boolean
ariaLabel?: string
}
const Modal: React.FC<Props> = ({
children,
ariaLabel = 'Alert Modal',
isOpen,
}) => (
<ReactModal
appElement={document.getElementById('root') as HTMLElement}
ariaHideApp={process.env.NODE_ENV !== 'test'}
isOpen={isOpen}
contentLabel={ariaLabel}
testId="modal-content"
>
{children}
</ReactModal>
)
export default Modal
Usage in component with state = { isOpen: true }:
<Modal isOpen={this.state.isOpen}>
<p>
Modal Content hereā€¦
</p>
<button onClick={() => { this.setState({ isOpen: false }) }}>Okay</button>
</Modal>
If getting the Warning: react-modal: App element is not defined... error when running tests (we were running Jest), you can suppress the warnings by adding the following to your test file:
import ReactModal from 'react-modal';
ReactModal.setAppElement('*'); // suppresses modal-related test warnings.
The shortest solution is to add
appElement={document.getElementById("hereIsYourRootElementId")}
It lets react-modal know where is your root element.
For reference, since it was a pain for me, if you are doing SSR, use the following code to prevent errors server-side:
if (typeof(window) !== 'undefined') {
ReactModal.setAppElement('body')
}
You could put this in componentDidMount() anywhere you use a modal or I put it in a custom modal component so it's nice and DRY.
Just put this
Modal.setAppElement('#root')
This will solve the warning. The root element coming from inside public folder index.html.
you need to add # before your root element id.
import React from 'react';
import Modal from 'react-modal';
Modal.setAppElement('#root');
const OptionModal = (props) => (
<Modal
isOpen={!!props.selectedOption}
contentLabel='this is the selected option'
>
<h3>Selected Option</h3>
{props.selectedOption && <p>{props.selectedOption}</p>}
<button onClick = {props.handleCloseOptionModal}>Close</button>
</Modal>
);
export default OptionModal;
here is the reference:
http://reactcommunity.org/react-modal/accessibility/
If you get that warning on testing with the "react-testing-library" here is a solution:
https://github.com/reactjs/react-modal/issues/576#issuecomment-524644035
using the react-testing-library (https://testing-library.com/) I get rid of that warning with:
import Modal from "react-modal";
const { container } = render(<MyComponent />);
Modal.setAppElement(container);
.... // to the testing, use Modal
or, if you want to test the modal component directly:
const { container, rerender } render(<MyModalComponent isOpen={false} />);
Modal.setAppElement(container);
// now the appElement is set we can show the modal component
rerender(<MyModalComponent isOpen={false} />);
.... // to the testing
For Nextjs, I think you can solve this by adding the below to outside your modal component, maybe on top, before the component is declared.
Modal.setAppElement('#__next')
Delete this attrib
className="modal"
and run again

react-s-alert, alerts won't popup

So i got a meteor setup with a trello like GUI, based on blaze but with react components and i want to use this library https://www.npmjs.com/package/react-s-alert for showing some popups (feel free to suggest react alternatives).
So i did this, inside a header bar inside client/components/main/header.js:
import React from 'react';
import Alert from 'react-s-alert';
import 'react-s-alert/dist/s-alert-default.css';
import 'react-s-alert/dist/s-alert-css-effects/slide.css';
...
Template.header.events({
'click .js-create-board': Popup.open('createBoard'),
'click .js-alert'() {
Alert.info('Test message')
},
});
But when the function is called, nothing happens..
Anyone can help?
I think you haven't added its initializer component in your main component.
Place sAlert component in your main app renderer component also.
<Alert stack={{limit: 3}} />

When do React PropTypes get initialized?

I am having an issue with required PropTypes throwing errors. I would expect the PropType to throw an error if the Component was was being directly rendered. Here is a small sample of what I am trying to achieve.
You'll notice that the Button prop has a required PropType of handle click.
But I want the implementation of a Modal to be as simple as possible.
And since I don't have the context of Modal I can not bind the handleClick method directly to the Button so I pass the Button in as child and map over the children adding the handleClick method to the child component. This works pretty well besides throwing the error for Button because <Button> gets called and checked before it truly gets rendered.
I have tried to do this a few other ways as well using Higher Order Components which worked as well. But the implementation seemed convoluted and tedious this seems like a much simpler way to just generate a Modal when it is needed. You don't need any props you just pass in a child component and it will add the click handler.
It would be awesome to either Bypass the proptypes check until it is actually rendered in Modal Component or maybe there is a simpler way, all feed back is welcomed.
https://jsfiddle.net/kriscoulson/00xLw0up/1/
var Button = (props) =>
<button onClick={props.handleClick}>
{props.children}
</button>
Button.propTypes = {
handleClick: React.PropTypes.func.isRequired
}
class Modal extends React.Component {
openModal () {
console.log('Opening Modal.....')
}
childrenWithProps () {
return React.Children.map(this.props.children,(child) => React.cloneElement(child, {
handleClick: this.openModal
})
);
}
render () {
return (
<div>
{this.childrenWithProps()}
</div>
);
}
}
var App = () =>
<Modal>
<Button>Launch Modal</Button>
</Modal>
ReactDOM.render(
<App/>,
document.getElementById('container')
);
<script src="https://facebook.github.io/react/js/jsfiddle-integration-babel.js"></script>
<div id="container">
<!-- This element's contents will be replaced with your component. -->
</div>

Categories

Resources