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.
Related
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
Counter File
import React, { useState } from "react";
import Widget from "./widget";
const Counter = () => {
const [form, setForm] = useState(<></>)
const [text, setText] = useState("")
const onCounterChange =() => {
setText(text)
}
const formLoad =() =>{
setForm(
<Widget
onCounterChange={onCounterChange}
children={
<input type="text" onChange={(e) =>{
setText(e.target.value)
}}/>
}
/>
)
}
return (
<div>
{text}
<button onClick={formLoad}>
load widget
</button>
{form}
</div>
)
}
export default Counter
Widget File
import React from 'react'
export default function Widget(props) {
return (
<div className="buttons">
{props.children}
<button onClick={props.onCounterChange}>Save</button>
</div>
)
}
I have created small text printing page . for some purpose I have added children in a diff component and handling widget in a state , so when I try to change the data , text state is changing but when I click save text state becomes empty
As I mentioned in my comment putting a component in state probably isn't the best way of approaching this. Instead I would have a boolean state that allows you to toggle the component on/off.
const { useState } = React;
function Example() {
const [ showWidget, setShowWidget ] = useState(false);
const [ text, setText ] = useState('');
function handleChange(e) {
setText(e.target.value);
}
function handleClick() {
setShowWidget(!showWidget);
}
function handleSave() {
console.log(`Saved state: ${text}`);
}
return (
<div>
<p className="text">Current state: {text}</p>
<button onClick={handleClick}>
Load widget
</button>
{showWidget && (
<Widget>
<input
type="text"
onChange={handleChange}
/>
<button onClick={handleSave}>Save</button>
</Widget>
)}
</div>
);
}
function Widget({ children }) {
return <div className="widget">{children}</div>;
}
ReactDOM.render(
<Example />,
document.getElementById('react')
);
.widget { margin-top: 1em; }
.text { margin-bottom: 1em; }
<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>
I have a list of movie cards when a user clicks on them, they become selected and the id of each movie card is transferred to an array named "selectedList".
I want to add a "let's go" button below the movie card but conditionally.
I mean when the array is empty the button should not be displayed and when the user clicked on at least a movie the button displays. the array should be checked each time and whenever it becomes equal to zero the button should disappear.
the thing is all the movie cards are the children of this page and I want to render the parent component based on children's behavior.
MY MAIN PAGE:
export default function Index(data) {
const info = data.data.body.result;
const selectedList = [];
return (
<>
<main className={parentstyle.main_container}>
<NavBar />
<div className={style.searchbar_container}>
<SearchBar />
</div>
<div className={style.card_container}>
{info.map((movie, i) => {
return (
<MovieCard
movieName={movie.name}
key={i}
movieId={movie._id}
selected={selectedList}
isSelected={false}
/>
);
})}
</div>
</main>
<button className={style.done}>Let's go!</button>
</>
);
}
**MOVIE CARD COMPONENT:**
export default function Index({ selected, movieName, movieId, visibility }) {
const [isActive, setActive] = useState(false);
const toggleClass = () => {
setActive(!isActive);
};
const pushToSelected = (e) => {
if (selected.includes(e.target.id)) {
selected.splice(selected.indexOf(e.target.id), 1);
console.log(selected);
} else {
selected.push(e.target.id);
console.log(selected);
}
toggleClass();
};
return (
<div>
<img
className={isActive ? style.movie_selected : style.movie}
src={`images/movies/${movieName}.jpg`}
alt={movieName}
id={movieId}
onClick={pushToSelected}
/>
<h3 className={style.title}>{movieName}</h3>
</div>
);
}
You can use conditional rendering for that:
{selectedList.length > 0 && <button className={style.done}>Let's go!</button>}
Plus, you should change your selectedList to a state, and manage the update via the setSelectedList function:
import { useState } from 'react';
export default function Index(data) {
const info = data.data.body.result;
const [selectedList, setSelectedList] = useState([]);
Add the method to the MovieCardĀ as a property:
<MovieCard
movieName={movie.name}
key={i}
movieId={movie._id}
selected={selectedList}
setSelected={setSelectedList}
isSelected={false}
/>;
And update the list in the pushToSelected method:
export default function MovieCard({
selected,
setSelected,
movieName,
movieId,
visibility
}) {
const pushToSelected = (e) => {
if (selected.includes(e.target.id)) {
selected.splice(selected.indexOf(e.target.id), 1);
console.log(selected);
} else {
selected.push(e.target.id);
console.log(selected);
}
setSelected([...selected]);
toggleClass();
};
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);
};
}, []);
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>