I have created a basic modal using react without any library and it works perfectly, now when I click outside of the modal, I want to close the modal.
here is the CodeSandbox live preview
my index.js:
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
class App extends React.Component {
constructor() {
super();
this.state = {
showModal: false
};
}
handleClick = () => {
this.setState(prevState => ({
showModal: !prevState.showModal
}));
};
render() {
return (
<>
<button onClick={this.handleClick}>Open Modal</button>
{this.state.showModal && (
<div className="modal">
I'm a modal!
<button onClick={() => this.handleClick()}>close modal</button>
</div>
)}
</>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
The easiest way to get this is to call the closeModal function in the wrapper and stop propagation in the actual modal
For example
<ModalWrapper onClick={closeModal} >
<InnerModal onClick={e => e.stopPropagation()} />
</ModalWrapper>
Without using ref, it would be a little tricky
Watch this CodeSandBox
Or
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
class App extends React.Component {
constructor() {
super();
this.state = {
showModal: false
};
}
handleClick = () => {
if (!this.state.showModal) {
document.addEventListener("click", this.handleOutsideClick, false);
} else {
document.removeEventListener("click", this.handleOutsideClick, false);
}
this.setState(prevState => ({
showModal: !prevState.showModal
}));
};
handleOutsideClick = e => {
if (!this.node.contains(e.target)) this.handleClick();
};
render() {
return (
<div
ref={node => {
this.node = node;
}}
>
<button onClick={this.handleClick}>Open Modal</button>
{this.state.showModal && (
<div className="modal">
I'm a modal!
<button onClick={() => this.handleClick()}>close modal</button>
</div>
)}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
You can do it by creating a div for the modal backdrop which sits adjacent to the modal body. Make it cover the whole screen using position absolute and 100% height and width values.
That way the modal body is sitting over the backdrop. If you click on the modal body nothing happens because the backdrop is not receiving the click event. But if you click on the backdrop, you can handle the click event and close the modal.
The key thing is that the modal backdrop does not wrap the modal body but sits next to it. If it wraps the body then any click on the backdrop or the body will close the modal.
const {useState} = React;
const Modal = () => {
const [showModal,setShowModal] = useState(false)
return (
<React.Fragment>
<button onClick={ () => setShowModal(true) }>Open Modal</button>
{ showModal && (
<React.Fragment>
<div className='modal-backdrop' onClick={() => setShowModal(false)}></div>
<div className="modal">
<div>I'm a modal!</div>
<button onClick={() => setShowModal(false)}>close modal</button>
</div>
</React.Fragment>
)}
</React.Fragment>
);
}
ReactDOM.render(
<Modal />,
document.getElementById("react")
);
.modal-backdrop {
position: absolute;
top: 0;
left: 0;
background: #252424cc;
height: 100%;
width: 100vw;
}
.modal {
position: relative;
width: 70%;
background-color: white;
border-radius: 10px;
padding: 20px;
margin:20px auto;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Please see the attached Codesandbox for a working example.
You were almost there. Firstly, you need to do a callback function in your handleClick() that will add a closeMenu method to the document:
handleClick = event => {
event.preventDefault();
this.setState({ showModal: true }, () => {
document.addEventListener("click", this.closeMenu);
});
};
And then toggle the state inside closeMenu():
closeMenu = () => {
this.setState({ menuOpen: false }, () => {
document.removeEventListener('click', this.closeMenu);
});
}
Any time you click outside of the component, then it'll close it. :)
This worked for me:
const [showModal, setShowModal] = React.useState(false)
React.useEffect(() => {
document.body.addEventListener('click', () => {
setShowModal(false)
})
})
return <>
<Modal
style={{ display: showModal ? 'block' : 'none'}}
onClick={(e) => e.stopPropagation()}
/>
<button onClick={(e) => {
e.stopPropagation()
setShowModal(true)
}}>Show Modal</button>
</>
This works for me:
Need to use e.stopPropagation to prevent loop
handleClick = e => {
if (this.state.showModal) {
this.closeModal();
return;
}
this.setState({ showModal: true });
e.stopPropagation();
document.addEventListener("click", this.closeModal);
};
then:
closeModal = () => {
this.setState({ showModal: false });
document.removeEventListener("click", this.closeModal);
};
Hope will help
This is how I solved it:
BTW, I"m a junior dev, so check it, GL.
In index.html:
<div id="root"></div>
<div id="modal-root"></div>
In index.js:
ReactDOM.render(
<React.StrictMode>
<ModalBase />
</React.StrictMode>,
document.getElementById("modal-root")
);
In the App.js:
const [showModal, setShowModal] = useState(false);
{showModal && (
<ModalBase setShowModal={setShowModal}>
{/*Your modal goes here*/}
<YourModal setShowModal={setShowModal} />
</ModalBase>
In the modal container:
import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
const modalRoot: HTMLElement | null = document.getElementById("modal-root");
const Modal: React.FC<{
children: React.ReactNode;
setShowModal: React.Dispatch<boolean>;
}> = ({ children, setShowModal }) => {
const [el] = useState(document.createElement("div"));
const outClick = useRef(el);
useEffect(() => {
const handleOutsideClick = (
e: React.MouseEvent<HTMLDivElement, MouseEvent> | MouseEvent
) => {
const { current } = outClick;
console.log(current.childNodes[0], e.target);
if (current.childNodes[0] === e.target) {
setShowModal(false);
}
};
if (modalRoot) {
modalRoot.appendChild(el);
outClick.current?.addEventListener(
"click",
(e) => handleOutsideClick(e),
false
);
}
return () => {
if (modalRoot) {
modalRoot.removeChild(el);
el.removeEventListener("click", (e) => handleOutsideClick(e), false);
}
};
}, [el, setShowModal]);
return ReactDOM.createPortal(children, el);
};
export default Modal;
Use the following onClick method,
<div className='modal-backdrop' onClick={(e) => {
if (e.target.className === 'modal-backdrop') {
setShowModal(false)
}
}}></div>
<div className="modal">
<div>I'm a modal!</div>
<button onClick={() => setShowModal(false)}>close modal</button>
</div>
</div>
.modal-backdrop {
position: absolute;
top: 0;
left: 0;
background: #252424cc;
height: 100%;
width: 100vw;
}
You can check the event.target.className if it's contain the parent class you can close the Modal as below, in case you clicked inside the popup div it will not closed:
handleClick = () => {
if (e.target.className === "PARENT_CLASS") {
this.setState(prevState => ({
showModal: false
}));
}
// You should use e.stopPropagation to prevent looping
e.stopPropagation();
};
I will explain with functional components:
first create ref to get a reference to the modal element
import { useEffect, useState, useRef } from "react";
const [isModalOpen,setIsModalOpen]=useState(false)
const modalEl = useRef();
<div className="modal" ref={modalEl} >
I'm a modal!
<button onClick={() => this.handleClick()}>close modal</button>
</div>
second in useEffect create an event handler to detect an event outside the modal element. For this we need to implement capture phase on an element. (explained here: What is event bubbling and capturing? ). Basically, we are going to register an event handler so that when the browser detects any event, browser will start to look for the event handlers from the top parent HTML element and if it finds it, it will call it.
useEffect(() => {
const handler = (event) => {
if (!modalEl.current) {
return;
}
// if click was not inside of the element. "!" means not
// in other words, if click is outside the modal element
if (!modalEl.current.contains(event.target)) {
setIsModalOpen(false);
}
};
// the key is using the `true` option
// `true` will enable the `capture` phase of event handling by browser
document.addEventListener("click", handler, true);
return () => {
document.removeEventListener("click", handler);
};
}, []);
Related
I'm midway through creating shopping cart of some website. I want the shopping cart to close when clicked outside of it. I managed to get it working, but for some reason it still closes when clicked on the div of the cart. I managed to find the problem. But i need help solving it. The logic behind it is that when website is clicked I get the event using listener and try to find if ref contains the click. Here's the code:
function CartItem(props) {
// handling outside clicks
const [open, setOpen] = useState(false);
const cartRef = useRef(null);
useEffect(() => {
document.addEventListener("click", handleClickOutside, true)
}, []);
const handleClickOutside = (e) => {
if(!cartRef.current.contains(e.target)) {
setOpen(false);
}
}
return (
<div className='nav-item' ref={cartRef} onClick={() => setOpen(!open)} >
<div className='navItemImage'>
<img src={CartImage} alt="Shopping Cart Icon" title="Shopping Cart Icon"/>
</div>
{open && props.children}
</div>
);
}
Now the problem is that when click happens on the opened nav-item DIV (which is the shopping cart DIV) props.children isn't rendered, thus .contains is false and changes the state. What can I do to solve this problem. Thanks!
Here is the full code for context:
import React, { useEffect, useState, useRef } from 'react';
import "./Cart.css";
import CartImage from "../../assets/Cart.svg";
import { findCurrencySymbol } from "../../constructors/functions"
import { findAmount } from "../../constructors/functions"
function Cart(props) {
return(
<CartItem >
<CartMenu cart={props.cart} currencyLabel={props.currencyLabel}/>
</CartItem>
);
}
function CartItem(props) {
// handling outside clicks
const [open, setOpen] = useState(false);
const cartRef = useRef(null);
useEffect(() => {
document.addEventListener("click", handleClickOutside, true)
}, []);
const handleClickOutside = (e) => {
console.log(cartRef.current);
console.log(e.target);
if(!cartRef.current.contains(e.target)) {
setOpen(false);
}
}
return (
<div className='nav-item' ref={cartRef} onClick={() => setOpen(!open)} >
<div className='navItemImage'>
<img src={CartImage} alt="Shopping Cart Icon" title="Shopping Cart Icon"/>
</div>
{open && props.children}
</div>
);
}
function CartMenu(props) {
function CartMenuItem(props) {
return (
<div className='cart-item'>
<h1 className='cart-header'>My bag, <span>{props.cart.length} items</span></h1>
<ul className='cart-products'>
{
props.cart.map((product) => {
return (
<li className='cart-product' key={product}>
<div className='cart-left'>
<h2 className='cart-product-title'>{product.brand}</h2>
<h2 className='cart-product-title'>{product.name}</h2>
<h2 className='cart-product-price'>{findCurrencySymbol(product.prices, props.currencyLabel)} {findAmount(product.prices, props.currencyLabel)}</h2>
{/* {
product.category.map((item) => {
return (
console.log(item)
);
})
} */}
{/* {console.log(product)} */}
</div>
<div className='cart-middle'>
<div className='quantity-increase' onClick={() => increaseQuantity()}>+</div>
<h2>{product.quantity}</h2>
<div className='quantity-decrease' onClick={() => decreaseQuantity()}>-</div>
</div>
<div className='cart-right'>
<img src={product.image} />
</div>
</li>
);
})
}
</ul>
</div>
);
}
return (
<div className='dropdown-cart'>
{props.cart.map((item) => {
return (
<CartMenuItem cart={props.cart} key={item} currencyLabel={props.currencyLabel}/>
);
})}
</div>
);
}
function increaseQuantity() {
console.log(+1);
}
function decreaseQuantity() {
console.log(-1);
}
export default Cart;
Here is the console logged cartRef and e.target:
I tried using forwarding refs but got similar results
NOTE: You can view and edit the code in CodeSandbox.
I have the following parent file which creates a useState list of child component called ProgressBar:
import React, { useState } from 'react';
import ProgressBar from './ProgressBar';
import './App.css';
var idCounter = 0;
export default function App() {
const [barsArray, setBarsArray] = useState([]);
const [input, setInput] = useState('');
function add() {
setBarsArray((prev) => [
...prev,
<ProgressBar key={idCounter++} restart={false} />,
]);
}
function remove() {
setBarsArray((prev) => prev.filter((bar) => bar.key !== input));
}
function reset() {
setBarsArray((prev) =>
prev.map((bar) => (bar.key === input ? { ...bar, restart: true } : bar))
);
}
return (
<div className='App'>
<div className='buttons'>
<button className='button-add' onClick={add}>
Add
</button>
<button className='button-delete' onClick={remove}>
Delete
</button>
<button className='button-delete' onClick={reset}>
Reset
</button>
<input
type='number'
value={input}
onInput={(e) => setInput(e.target.value)}
/>
</div>
<div className='bars-container'>
{barsArray.map((bar) => (
<div className='bars-index' key={bar.key}>
{bar}
<p>{bar.key}</p>
</div>
))}
</div>
</div>
);
}
The file of the child ProgressBar has the following content:
import React, { useEffect, useState } from 'react';
import './ProgressBar.css';
export default function ProgressBar(props) {
const [progress, setProgress] = useState(0);
let interval;
useEffect(() => {
interval = setInterval(() => {
setProgress((prev) => prev + 1);
}, RnadInt(10, 120));
}, []);
useEffect(() => {
if (progress >= 100) clearInterval(interval);
}, [progress]);
if (props.restart === true) {
setProgress(0);
}
return (
<>
<div className='ProgressBarContainer'>
<div className='ProgressBar' style={{ width: progress + '%' }}></div>
</div>
</>
);
}
function RnadInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
My problem is that the reset button in the parent doesn't work, as far as I'm concerned, if you change the passed props to the child, the child automatically re-renders, but even though I'm updating the props in reset function in the parent, which maps the old array of child components to a new array and only changes the props of the selected child.
Thanks!
Adding the element in state via add would require to keep the ref of element instead of actual prop bind to element. Suggestion here to use the model object and while rendering use the JSX element.
Please use the below code which defines the barsArray as object state and later uses it render ProgressBar component (from map call).
Check the working codesandbox - https://codesandbox.io/s/admiring-glitter-j3b7sw?file=/src/App.js:0-1446
import React, { useState } from "react";
import ProgressBar from "./ProgressBar";
import "./App.css";
var idCounter = 0;
export default function App() {
const [barsArray, setBarsArray] = useState([]);
const [input, setInput] = useState("");
function add() {
setBarsArray((prev) => [...prev, { id: idCounter++, restart: false }]);
}
function remove() {
setBarsArray((prev) =>
prev.filter((bar) => bar.id.toString() !== input.toString())
);
}
function reset() {
setBarsArray((prev) =>
prev.map((bar) => {
return bar.id.toString() === input.toString()
? { ...bar, restart: true }
: { ...bar };
})
);
}
return (
<div className="App">
<div className="buttons">
<button className="button-add" onClick={add}>
Add
</button>
<button className="button-delete" onClick={remove}>
Delete
</button>
<button className="button-delete" onClick={reset}>
Reset
</button>
<input
type="number"
value={input}
onInput={(e) => setInput(e.target.value)}
/>
</div>
<div className="bars-container">
{barsArray.map((bar) => (
<div className="bars-index" key={bar.id}>
<ProgressBar key={bar.id} restart={bar.restart} />
<p>{bar.key}</p>
</div>
))}
</div>
</div>
);
}
This simple fix worked for me, in the file of the child, call the conditional expression in a useEffect hook. Currently, it doesn't listen to the changes in props.restart and only runs on the initial render.
check https://reactjs.org/docs/hooks-reference.html#useeffect
useEffect(() => {
if (props.restart === true) {
setProgress(0);
}
}, [props.restart])
link to working codesandbox https://codesandbox.io/s/distracted-gauss-24d0pu
I am very new to React and this is my first task on an existing project..! Okay, coming to the point, So there is a Parent component ContactForm.js that has child component UserInfoStep.js. There is a button in parent component that I need to enable/disable based on a toggle button in the child component. I have tried using callback but it gives me error..
TypeError: Cannot read properties of undefined (reading 'callbackFunction')
ContactForm.js
const callbackFunction = (isChecked) => {
//do enable/disable
this.Button.disabled = isChecked;
};
function ContactForm() {
const [buttonState, setButtonState] = useState(true);
const { PUID,FirstName, LastName, EurofinsLegalEntity, EurofinsBusinessUnit, StartDate, EndDate, frequency, test, exp, signed, laptop, transfer, existing, newL, categories} = state;
const steps = [
<UserInfoStep parentCallback = {this.callbackFunction} {...{ PUID,FirstName, LastName, EurofinsLegalEntity, EurofinsBusinessUnit, StartDate, EndDate}} />];
return (
<ContactFormContext.Provider value={{ dispatch }}>
//some code removed
<Button
disabled={buttonState}
type="submit"
className={classes.button}
color="primary"
variant="outlined"
onClick={e => {
e.preventDefault();
if (isLast) {
handleSubmit();
} else {
goForward();
}
}}
>
{isLast ? "Submit" : "Next"}
</Button>
//some code removed
</ContactFormContext.Provider>
);
}
export default ContactForm;
UserInfoStep.js
function UserInfoStep ({ PUID, FirstName, LastName, EurofinsLegalEntity, EurofinsBusinessUnit, StartDate, EndDate }) {
const { dispatch } = useContext(ContactFormContext);
const [checked, setChecked] = useState(false);
const handleChange = () => {
setChecked((prev) => !prev);
//as soon as this is checked, enable the button in contactForm
sendData(checked);
};
const sendData = (isChecked) => {
this.parentCallback(isChecked);
//Enabled = isChecked;
};
return(
//some controls
<FormControlLabel
control={<Switch checked={checked} onChange={handleChange} />}
/>
);
}
export default UserInfoStep;
You're using function components so there's no need to use this.
Your state isn't being used because...
...your callback function is outside of the component and trying to set this.Button which doesn't exist.
So (this is a very simplified version of your code): move the handler inside the component so that it can update the state directly, and use the button state to inform the disabled property of the button which state it should be in.
const { useState } = React;
function UserInfoStep({ handleChange }) {
return (
<button onClick={handleChange}>Toggle parent button</button>
);
}
function ContactForm() {
const [ buttonState, setButtonState ] = useState(true);
function handleChange() {
setButtonState(!buttonState);
}
return (
<div>
<div class="child">
Child component
<UserInfoStep handleChange={handleChange} />
</div>
<div class="parent">
Parent component
<button disabled={buttonState}>
Parent button
</button>
</div>
</div>
);
};
ReactDOM.render(
<ContactForm />,
document.getElementById('react')
);
.child, .parent { margin: 0.5em 0 0.5em 0; }
.child button:hover { cursor: pointer; }
.parent button { background-color: #b3ffcc; }
.parent button:disabled { background-color: #ff9980; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
You can have a function inside the ContactForm component which toggles the buttonState. You can then pass that function as a prop to the child component and call that function for enabling/disabling the button.
in ContactForm component
For eg:
const toggler = () =>{
setButtonState(!buttonState);
}
pass this toggler function as a prop to the child component and attach it to the onClick handler of the toggle button.
I have a button that opens a div that looks like a modal.
On click of this button, I want to add a class on to the body, then on click of a close button, I want to remove this class.
How would you do this in React functional component with useEffect / useState?
You could use this.setState like this in Components
....
handleClick = () => {
this.setState((state) => {
addClass: !state.addClass
})
}
render () {
const { addClass } = this.state
return (
<>
<button onClick={this.handleClick}>Button</button>
<div className={!!addClass && 'yourClassName'}>hello</div>
<>
)
}
And in function like this
function Example() {
const [addClassValue, addClass] = useState(0);
return (
<div>
<button onClick={() => addClass(!addClassValue)}>
Click me
</button>
<div className={addClass ? 'yourClassName'}>hello</div>
</div>
)
}
But you do not need classes because React works with components and you can show your modal like
function Example() {
const [addClassValue, addClass] = useState(false);
return (
<div>
<button onClick={() => addClass(!addClassValue)}>
Click me
</button>
// this Component will be only rendered if addClassValue is true
{addClassValue && <ModalComponent />}
</div>
)
}
Currently I'm trying to avoid refreshing page by adding a preventDefault() call into the onClick handler of a functional component (BookList defined in bookList.js). I know I can make it with from class component to functional. However, is there any way to call preventDefault() in the onClick event handler in BookList?
Here is my sample code:
BookListElement.js
import React from 'react';
import { connect } from 'react-redux';
import BookList from '../components/bookList';
import { deleteBook } from '../store/actions/projectActions';
const BookListElement = ({books, deleteBook}) => {
if(!books.length) {
return (
<div>
No Books
</div>
)
}
return (
<div>
{Array.isArray(books) ? books.map(book => {
return (
<BookList book={book} deleteBook={deleteBook} key={book._id} />
);
}): <h1>something wrong.</h1>}
</div>
);
};
const mapStateToProps = state => {
return {
books: state.books
};
};
const mapDispatchToProps = dispatch => {
return {
deleteBook: _id => {
dispatch(deleteBook(_id));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(BookListElement);
bookList.js
import React, { Component } from 'react';
const styles = {
borderBottom: '2px solid #eee',
background: '#fafafa',
margin: '.75rem auto',
padding: '.6rem 1rem',
maxWidth: '500px',
borderRadius: '7px'
};
const BookList = ({ book: { author, publication, publisher, _id }, deleteBook }) => {
return (
<form>
<div className="collection-item" style={styles} key={_id}>
<h2>{author}</h2>
<p>{publication}</p>
<p>{publisher}</p>
<button className="btn waves-effect waves-light" onClick={() => {deleteBook(_id)}}>
<i className="large material-icons">delete_forever</i>
</button>
</div>
</form>
);
};
export default BookList;
action.js
export const deleteBookSuccess = _id => {
return {
type: DELETE_BOOK,
payload: {
_id
}
}
};
export const deleteBook = _id => {
return (dispatch) => {
return axios.delete(`${apiUrl}/${_id}`)
.then(response => {
dispatch(deleteBookSuccess(response.data))
})
.catch(error => {
throw(error);
});
};
};
reducer.js
case DELETE_BOOK:
let afterDelete = state.filter(book => {
return book._id !== action.payload._id
});
return afterDelete;
If you don't want a button to trigger form submission, add type="button" attribute to the button element.
By default a button submits the form (has type set to submit).
Setting type="button" signifies that it has no default behavior.
<form>
<button type="button">type button doesn't trigger refresh</button>
<button>no type triggers refresh</button>
</form>
The preventDefault method needs to be called on an event. However in the way you are setting up your onClick handler, the event is not passed to your handler.
Here is how you can fix this issue (in bookList.js):
import React from 'react';
const BookList = ({ book: { author, publication, publisher, _id }, deleteBook }) => {
const handleClick = event => {
event.preventDefault();
deleteBook(_id);
}
return (
<form>
<div>
<h2>{author}</h2>
<p>{publication}</p>
<p>{publisher}</p>
<button onClick={ handleClick }>
<i>delete_forever</i>
</button>
</div>
</form>
);
};
So onClick will pass the event (by default) then just call preventDefault on that event, and then call deleteBook.
I made a simple illustration (component) where one click event triggers the reload, and the other doesn't. If this doesn't work for you, the problem is another place, meaning in a different component(pieces of code you haven't provided). I hope it helps, if not maybe it will help someone in the future :)
class Example extends React.Component {
constructor(props) {
super(props);
this.preventDefault = this.preventDefault.bind(this);
this.dontPrevent = this.dontPrevent.bind(this);
}
// handler recieves the `e` event object
dontPrevent() {
alert('This page will reload :)');
}
preventDefault(e) {
alert('Page will NOT reload');
e.preventDefault();
}
render() {
return (
<div>
<form onSubmit={this.preventDefault}>
<div>
<h3>NO Reload Example</h3>
<p>
<button>Submit</button>
</p>
</div>
</form>
<hr/>
<form>
<div>
<h3>Reload Example</h3>
<p>
<button onClick={this.dontPrevent}>Submit</button>
</p>
</div>
</form>
</div>
);
}
}
ReactDOM.render(
<Example />,
document.getElementById('app')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>