How to set a ref.current on click? - javascript

EDIT: it should be show / hide 'Hi' on click of 'Open' so basically clicking 'Open' is equivalent to clicking 'Hello', I'm sorry.
I have a <details> element and a <p> element, what I want to do is on click of the <Menu /> component, toggle the <Details /> component on and off (show / hide 'Hi' on click of 'Hello'). Here is the code I've tried:
import React, { useState, useRef } from "react";
import ReactDOM from "react-dom";
const Menu = ({ toggleDetails }) => {
return (
<div>
<p onClick={toggleDetails}>Open</p>
</div>
);
};
const Details = (isOpen) => {
const detailsRef = useRef();
// detailsRef.current.open = isOpen;
return (
<details ref={detailsRef}>
<summary>Hello</summary>
<div>Hi</div>
</details>
);
};
const App = () => {
const [isOpen, setIsOpen] = useState(false);
const toggleDetails = () => {
setIsOpen(isOpen ? false : true);
};
return (
<div>
<Details isOpen={isOpen} />
<Menu toggleDetails={toggleDetails} />
</div>
);
};
ReactDOM.render(<App />, document.getElementById("container"));
A code sandbox: https://codesandbox.io/s/react-playground-forked-25ott?file=/index.js
I feel I'm close, on change of isOpen state, the Details component is re-rendered and I want to toggle detailsRef.current.open to true or false, but detailsRef.current is undefined so the commented line does not work, how can I achieve this?

If you mean what I think you mean when you say "toggle details on and off", you're looking for a conditional render:
const App = () => {
const [isOpen, setIsOpen] = useState(false);
const toggleDetails = () => {
setIsOpen(isOpen ? false : true);
};
return (
<div>
{isOpen && <Details />}
<Menu toggleDetails={toggleDetails} />
</div>
);
};
You don't need a ref at all, nor does Details need the isOpen prop.
To toggle the visibility of hi on click of hello, you would do the same:
const Details = (isOpen) => {
const [showHi, setShowHi] = useState(true)
return (
<details>
<summary onClick={() => setShowHi(!showHi)}>Hello</summary>
{showHi && <div>Hi</div>}
</details>
);
};

Related

Toggle functionality in React

So at the beginning I know this code is messy. I have a div that I wanna copy, when I click on the div, it opens up a Content and it copies that div. The thing I can't seem to accomplish is this, In that copied version of div/Image, I want to have a toggle functionality as in a regular one. If you don't fully understand it here is the sandbox https://codesandbox.io/s/epic-bassi-ogzjq. Thank you
const data= [
{
id:1,
img:"./images/icons/folder.png",
name:"Small Projects"
}
]
function App() {
const [isProjectOpen, setIsProjectOpen] = useState(false)
const project= useRef(null)
const [smallProject, setSmallProject] =useState()
const [active, setActive] = useState()
const closeProject = () =>{
setIsProjectOpen(false)
}
const proj = data.find(projects => projects.id===smallProject)
const handleProject=(e)=>{
if(project.current && !project.current.contains(e.target)){
closeProject()
}
}
useEffect(()=>{
document.addEventListener('click', handleProject)
return() =>{
document.removeEventListener('click', handleProject)
}
},[])
return (
<>
{data.map((projects)=>{
return(
<>
{active === project.id && (
<div
onClick={()=>setIsProjectOpen(!isProjectOpen)}
className='smallProject__container'
key={projects.id}
ref={project} >
<img
onClick={()=>setSmallProject(projects.id)}
className="smallProject__img"
src={projects.img}/>
<h4
onClick={()=>setSmallProject(projects.id)}
>{projects.name} </h4>
{isProjectOpen ? <SmallProject /> :null}
</div>)}
</>
)
})}
{smallProject && (
<div
onClick={()=>setIsProjectOpen(!isProjectOpen)}
className={"smallProjects__container " + ((isProjectOpen) ? "active": "")} >
<img
classsName='copy__img'
src={proj.img}/>
</div>
)}
</>
);
}
export default App;

How to refactor an if else if with previous state when using useState Hook?

I have 2 details tag, each has a control to toggle it on/off. Code snippet here. Clicking Control A should toggle on/off page A, clicking Control B should toggle on/off page B.
I did it with an if else if plus 2 useState, this would not be feasible when there are multiple details. How can I refactor the code such that maybe the if else if can be avoided and it detects which Control I click in a cleverer way?
Page.js
const Page = ({ name, isOpen, setIsOpen }) => {
return (
<>
<details
open={isOpen}
onToggle={(e) => {
setIsOpen(e.target.open);
}}
>
<summary>Page {name} title</summary>
<div>Page {name} contents</div>
</details>
</>
);
};
export default Page;
Control.js
const Control = ({ toggle }) => {
return (
<>
<a onClick={() => toggle("A")} href="#/">
Control A
</a>
<br />
<a onClick={() => toggle("B")} href="#/">
Control B
</a>
</>
);
};
App.js
export default function App() {
const [isOpenA, setIsOpenA] = useState(false);
const [isOpenB, setIsOpenB] = useState(false);
const toggle = (name) => {
if (name === "A") {
setIsOpenA((prevState) => !prevState);
} else if (name === "B") {
setIsOpenB((prevState) => !prevState);
}
};
return (
<div className="App">
<Control toggle={toggle} />
<Page name={"A"} isOpen={isOpenA} setIsOpen={setIsOpenA} />
<Page name={"B"} isOpen={isOpenB} setIsOpen={setIsOpenB} />
</div>
);
}
You can use an array to represent open ones
const [openPages, setOpenPages] = useState([])
And to toggle filter the array
const toggle = (name) => {
if(openPages.includes(name)){
setOpenPages(openPages.filter(o=>o!=name))
}else{
setOpenPages(pages=>{ return [...pages,name]}
}
}
I would personally use an object as a map for your toggles as in something like:
const [isOpen, _setIsOpen] = useState({});
const setIsOpen = (pageName,value) => _setIsOpen({
...isOpen,
[pageName]: value
});
const toggle = (name) => setIsOpen(name, !isOpen[name]);
and then in the template part:
<Page name={"A"} isOpen={isOpen["A"]} setIsOpen={toggle("A")} />
In this way you can have as many toggles you want and use them in any way you want
I think this would be quite cleaner, also you should put the various page names in an array and iterate over them as in
const pageNames = ["A","B"];
{
pageNames.map( name =>
<Page name={name} isOpen={isOpen[name]} setIsOpen={toggle(name)} />)
}
At least that's how I would go about it
Adithya's answer worked for me.
For future reference, I put the full working code here. The onToggle attribute in Page.js is not needed. All required is passing correct true/false to open={isOpen} in Page.js.
App.js:
export default function App() {
const [openPages, setOpenPages] = useState([]);
const toggle = (name) => {
if (openPages.includes(name)) {
setOpenPages(openPages.filter((o) => o !== name));
} else {
setOpenPages((pages) => {
return [...pages, name];
});
}
};
return (
<div className="App">
<Control toggle={toggle} />
<Page name={"A"} isOpen={openPages.includes("A")} />
<Page name={"B"} isOpen={openPages.includes("B")} />
<Page name={"C"} isOpen={openPages.includes("C")} />
</div>
);
}
Page.js
const Page = ({ name, isOpen }) => {
return (
<>
<details open={isOpen}>
<summary>Page {name} title</summary>
<div>Page {name} contents</div>
</details>
</>
);
};
Control.js remains the same.

How to pass HTML attributes to child component in React?

I have a parent and a child component, child component has a button, which I'd like to disable it after the first click. This answer works for me in child component. However the function executed on click now exists in parent component, how could I pass the attribute down to the child component? I tried the following and it didn't work.
Parent:
const Home = () => {
let btnRef = useRef();
const handleBtnClick = () => {
if (btnRef.current) {
btnRef.current.setAttribute("disabled", "disabled");
}
}
return (
<>
<Card btnRef={btnRef} handleBtnClick={handleBtnClick} />
</>
)
}
Child:
const Card = ({btnRef, handleBtnClick}) => {
return (
<div>
<button ref={btnRef} onClick={handleBtnClick}>Click me</button>
</div>
)
}
In general, refs should be used only as a last resort in React. React is declarative by nature, so instead of the parent "making" the child disabled (which is what you are doing with the ref) it should just "say" that the child should be disabled (example below):
const Home = () => {
const [isButtonDisabled, setIsButtonDisabled] = useState(false)
const handleButtonClick = () => {
setIsButtonDisabled(true)
}
return (
<>
<Card isDisabled={isButtonDisabled} onButtonClick={handleButtonClick} />
</>
)
}
const Card = ({isDisabled, onButtonClick}) => {
return (
<div>
<button disabled={isDisabled} onClick={onButtonClick}>Click me</button>
</div>
)
}
Actually it works if you fix the typo in prop of Card component. Just rename hadnlBtnClick to handleBtnClick
You don't need to mention each prop/attribute by name as you can use javascript Object Destructuring here.
const Home = () => {
const [isButtonDisabled, setIsButtonDisabled] = useState(false)
const handleButtonClick = () => {
setIsButtonDisabled(true)
}
return (
<>
<Card isDisabled={isButtonDisabled} onButtonClick={handleButtonClick} />
</>
)
}
const Card = (props) => {
return (
<div>
<button {...props}>Click me</button>
</div>
)
}
You can also select a few props and use them differently in the child components. for example, see the text prop below.
const Home = () => {
const [isButtonDisabled, setIsButtonDisabled] = useState(false)
const handleButtonClick = () => {
setIsButtonDisabled(true)
}
return (
<>
<Card text="I'm a Card" isDisabled={isButtonDisabled} onButtonClick={handleButtonClick} />
</>
)
}
const Card = ({text, ...restProps}) => {
return (
<div>
<button {...restProps}>{text}</button>
</div>
)
}

Gatsby: How to close previously opened item if new one is selected?

I have a page where are list items with option to click on each of them and open info box. When I click on one, info box is being opened, but when I click on another, the previously one stays there instead of closing it. How can I make it work, so when I click on new, the previously opened closes? My code here?
import React, { useState } from "react";
import "../../styles/styles.scss";
import InfoIcon from "../../images/icons/info.svg";
import InfoBox from "../info/InfoBox";
const Step = ({ title, description }) => {
const [show, setShow] = useState(false);
const openInfo = () => {
setShow(true);
};
const closeInfo = () => {
setShow(false);
};
return (
<>
<li>
<div className="list-item" onClick={openInfo}>
<div className="list-item-content">
<h3>{title}</h3>
<InfoIcon className="info-icon" />
</div>
</div>
</li>
{show && (
<InfoBox
title={title}
description={description}
closeInfo={closeInfo}
/>
)}
</>
);
};
export default Step;
You could:
move const [show, setShow] = useState(false); to the parent component.
Instead of a boolean you could store the title of the infoBox, initialising it to null . [openedInfo, setOpenedInfo] = useState(null);
the Step component will have 2 other props: setOpenedInfo and openedInfo
openInfo will be const openInfo = () => setOpenedInfo(title)
closeInfo will be const closeInfo = () => setOpenedInfo(null)
You will show the infoBox if openedStep === title
This way you will always have only one infoBox open.
I'm assuming that the title is a string and is unique. You can substitute title with any other (unique) value related to the Step component.
import React, { useState } from "react";
import "../../styles/styles.scss";
import InfoIcon from "../../images/icons/info.svg";
import InfoBox from "../info/InfoBox";
const ParentComponent = () => {
[openedInfo, setOpenedInfo] = useState(null);
return data.map(({title,description})=><Step title={title} description={description} setOpenedInfo={setOpenedInfo} openedInfo={openedInfo}/>)
}
const Step = ({ title, description, setOpenedInfo, openedInfo }) => {
const openInfo = () => {
setOpenedInfo(title); //better use an id if available
};
const closeInfo = () => {
setOpenedInfo(null);
};
return (
<>
<li>
<div className="list-item" onClick={openInfo}>
<div className="list-item-content">
<h3>{title}</h3>
<InfoIcon className="info-icon" />
</div>
</div>
</li>
{openedInfo === title && (
<InfoBox
title={title}
description={description}
closeInfo={closeInfo}
/>
)}
</>
);
};
export default Step;

How to test prop function that changes other prop Jest Enzyme

I have a component that receives value 'openDrawer' (bool) and function 'toggleDrawerHandler' in props, the function 'toggleDrawerHandler' changes the value of 'openDrawer' prop.
I would like to test it by simulating a click on div that triggers this function, and check if the component change when the value of 'openDrawer' changes.
The component
const NavigationMobile = (props) => {
const { openDrawer, toggleDrawerHandler } = props;
let navClass = ["Nav-Mobile"];
if (!openDrawer) navClass.push("Close");
return (
<div className="Mobile">
<div className="Menubar" onClick={toggleDrawerHandler}>
{openDrawer ? <FaTimes size="1.5rem" /> : <FaBars size="1.5rem" />}
</div>
<nav className={navClass.join(" ")} onClick={toggleDrawerHandler}>
<Navigation />
</nav>
</div>
);
};
The component that sends these props
const Header = (props) => {
const [openDrawer, setOpenDrawer] = useState(false);
const toggleDrawerHandler = () => {
setOpenDrawer((prevState) => !prevState);
};
return (
<header className="Header">
<NavigationMobile openDrawer={openDrawer} toggleDrawerHandler={toggleDrawerHandler} />
</header>
);
};
my test, but doesn't work
it("changes prop openDrawer when click", () => {
const wrapper = shallow(<NavigationMobile />);
expect(wrapper.find("FaBars")).toHaveLength(1);
expect(wrapper.find("nav").hasClass("Nav-Mobile")).toBeTruthy();
wrapper.find(".Menubar").simulate("click", true); // doesnt work
expect(wrapper.find("FaTimes")).toHaveLength(1);
expect(wrapper.find("nav").hasClass("Nav-Mobile Close")).toBeTruthy();
});

Categories

Resources