Toggle class on a mapped element in react - reactjs

I am learning react and I have some doubts about changing the color onClick
How can I toggle the className of a <p> element that is being clicked? At the moment clicking on one changes the color on everything.
I have read about passing a state on a parent element but at this point is getting a bit confusing and if someone could help me clarifying this would be nice.
Here's the code of my child element:
import "./Answer.scss";
function Answer(props) {
const getAnswers = [];
const correct_answer = props.correct_answer;
getAnswers.push(correct_answer);
for (let i = 0; i < props.incorrect_answers.length; i++) {
getAnswers.push(props.incorrect_answers[i]);
}
const [answers, setAnswer] = useState(getAnswers);
const [active, setActive] = useState(true);
const displayAnswers = answers.map((answer, index) => (
<p
className={active ? "answer" : "answer-active"}
key={index}
onClick={() => setActive((prevState) => !prevState)}
>
{answer.replace(/"|'/g, '"')}
</p>
));
return <div className="answer-box">{displayAnswers}</div>;
}
export default Answer;
And this is the parent:
import Answer from "../Answer/Answer";
function Questions(props) {
const questions = props.questions.map((question, index) => {
return (
<div className="question" key={index}>
<h2>{question.question.replace(/"|'/g, '"')}</h2>
<Answer
incorrect_answers={question.incorrect_answers}
correct_answer={question.correct_answer}
/>
<hr></hr>
</div>
);
});
return <div className="questions-container">{questions}</div>;
}
export default Questions;
Thanks everyone

Check this code.
const [answers, setAnswer] = useState(getAnswers);
const [active, setActive] = useState(true);
const displayAnswers = answers.map((answer, index) => (
<p
className={active ? "answer" : "answer-active"}
key={index}
onClick={() => setActive((prevState) => !prevState)}
>
{answer.replace(/"|'/g, '"')}
</p>
));
You are iterating your answers and updating the same state variable for each answer. It's like you are over-writing the updated values. Instead, you can make a separate state variable for each and every option. Based on the onClick, you can update that specific answer state and use it in the code. Check the below code.
const AnswerText = ({ valid, index, answer }) => {
const [active, setActive] = useState(valid);
return (
<p
className={active ? "answer" : "answer-active"}
key={index}
onClick={() => setActive((prevState) => !prevState)}
>
{answer}
</p>
);
};
You can use the above component in the map and display your answers.
Attached is a sandbox for reference.

Related

How to toggle class of a single element in a .map() function?

I am trying to toggle a class for a specific element inside a loop.
const ItemList: React.FC<ListItemUserProps> = (props) => {
const { items } = props;
const [showUserOpt, setShowUserOpt] = useState<boolean>(false);
function toggleUserOpt() {
setShowUserOpt(!showUserOpt);
}
const userOptVisible = showUserOpt ? 'show' : 'hide';
return (
<>
{items.map((t) => (
<React.Fragment key={t.userId}>
<div
className={`item ${userOptVisible}`}
role="button"
tabIndex={0}
onClick={() => toggleUserOpt()}
onKeyDown={() => toggleUserOpt()}
>
{t.userNav.firstName}
</div>
</React.Fragment>
))}
</>
);
};
export default ItemList;
When I click on an element, the class toggles for every single one.
You can create another component that can have it's own state that can be toggled without effecting other sibling components' state:
Child:
const ItemListItem: React.FC<SomeInterface> = ({ item }) => {
const [show, setShow] = useState<boolean>(false);
const userOptVisible = show ? "show" : "hide";
const toggleUserOpt = (e) => {
setShow((prevState) => !prevState);
};
return (
<div
className={`item ${userOptVisible}`}
role="button"
tabIndex={0}
onClick={toggleUserOpt}
onKeyDown={toggleUserOpt}
>
{item.userNav.firstName}
</div>
);
};
Parent:
const ItemList: React.FC<ListItemUserProps> = ({ items }) => {
return (
<>
{items.map((t) => (
<ItemListItem key={t.userId} item={t} />
))}
</>
);
};
If you simply adding classes to the element, I would keep it simple and use a handler to toggle the class using pure JS.
const handleClick = (e) => {
// example of simply toggling a class
e.currentTarget.classList.toggle('selected');
};
Demo:
const {
useState,
} = React;
// dummy data
const data = Array(20).fill(null).map((i, index) => `item ${(index + 1).toString()}`);
function App() {
const [items, setItems] = useState(data);
const handleClick = (e) => {
e.currentTarget.classList.toggle('selected');
};
return (
<div>
{items.map((item) => (
<button key={item} onClick={handleClick}>{item}</button>
))}
</div>
);
}
ReactDOM.render( <
App / > ,
document.getElementById("app")
);
.selected {
background: red;
}
<script crossorigin src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
<div id="app"></div>
I think it'd be best if you kept track of the index so that you could target a single item in your list. As it stands the boolean is going to change the styling for all as you haven't specified which one should get the className.
Add a useState hook to keep track of it like:
const [activeIndex, setActiveIndex] = useState(null);
Then create a new function:
function handleIndexOnClick(index) {
setActive(index);
}
Then in your map() function add index. You'll then need to pass index in to you className attribute and the onClick function. The end result for that bit should look like:
{items.map((t, index) => (
<React.Fragment key={t.userId}>
<div
className={`item ${activeIndex && items[activeIndex] ? 'show' : 'hide }`}
role="button"
tabIndex={0}
onClick={() => handleIndexOnClick(index)}
onKeyDown={() => toggleUserOpt()}
>
{t.userNav.firstName}
</div>
</React.Fragment>
))}

Custom Hook and cascading dropdown

I'm new to react and I'm learning by doing. Right now I'm stacked on using a custom Hook to share logic among components.
here is the logic inside the custom Hook
const useDropDownLogic = () => {
const { menu } = useContext(MenuContext)
const brand = [...new Set(menu.map(i => i.brand))]
const [category, setCategory] = useState([])
const brandChange = (e) => {
const { type, textContent } = e.target
setCategory([...new Set(menu.filter(i => i.[type] == textContent).map(i => i.category))])}
return {
brandChange,
brand,
category
}
}
export default useDropDownLogic;
here is the first component consuming the custom Hook
const DropDownBrand = () => {
const {brandChange, brand} = useDropDownLogic();
const dropdownRef = useRef(null);
const [isActive, setIsActive] = useDetectOutsideClick(dropdownRef, false);
const onClick = () => setIsActive(!isActive)
return (
<div id="dropdownbrand">
<div className="menu-container menu-brand">
<label className = 'nav-label'>Select a Brand</label>
<button onClick={onClick} className="menu-trigger brand-btn">
<i className="fas fa-chevron-down fa-2x drop-arrow"></i>
</button>
<nav
ref={dropdownRef}
className={`drop-menu ${isActive ? "active" : "inactive"}`}
>
<ul>
{brand.map((i) => (<li key={nanoid()} type = 'brand' value={i} onClick = {(e) => brandChange(e)}>{i}</li>))}
</ul>
</nav>
</div>
</div>
);
};
export default DropDownBrand;
and here the second
const DropDownCategory = () => {
const { category } = useDropDownLogic()
const dropdownRef = useRef(null);
const [isActive, setIsActive] = useDetectOutsideClick(dropdownRef, false);
const onClick = () => setIsActive(!isActive)
console.log(category);
return (
<div id="dropcategory">
<div className="menu-container menu-brand">
<label className = 'nav-label'>Select a Brand</label>
<button onClick={onClick} className="menu-trigger brand-btn">
<i className="fas fa-chevron-down fa-2x drop-arrow"></i>
</button>
<nav
ref={dropdownRef}
className={`drop-menu ${isActive ? "active" : "inactive"}`}
>
<ul>
{category.map((i) => (<li key={nanoid()} type = 'category'>{i}</li>))}
</ul>
</nav>
</div>
</div>
);
};
export default DropDownCategory;
Basically I'm able to populate the first dropdown but not the second. I don't understand why the category state is not update into the DropDownCategory component
any hint in what I'm doing wrong?
Many thanks in advance
I see,
I'm changing the approach. Moving state to a parent component and passing down function to handle click event. Then on Parent I'll update the dstate and pass it as props down to the child.
Nyhting to share yet but it will come
There is no persisted state in a hook, it's a way to share logic and functions. Because you are defining
const [category, setCategory] = useState([])
inside of the hook, it is defined each time you call the hook. In your case, dropdowncategory and dropdownbrand each have their personal state variable called category.
To solve this, define [category, setCategory] inside of the menu context, (or any other context) and pull in the values into your hook.. That way all instances of useDropDownLogic() are associated to global context value.

setting states in mapped elements react

I'm having a problem setting the states of other mapped elements to false when clicking on one of the individual mapped element. For example,
const [edit, setEdit] = useState(false)
const array = ['witch-king', 'sauron', 'azog']
const arrayrow = array && array.map(el=>{
return <div>
<i
className='fal fa-edit'
onClick={(e)=>{
setEdit(true);
e.stopPropagation()
}
></i>
{edit?<i className='fal fa-times'></i>:''}
<span>{el}</span>
</div>
})
useEffect(()=>{
document.addEventListener('click', ()=>{setEdit(false)})
},[])
The issue is that when you click on one of the icons it will set the state to true, but then if you click on another element's icon, the previously clicked element's state will remain true. I want to be able to set the previously clicked element's state to false when the user clicks on another element's icon.
EDIT: here is a video further explaining what I mean
https://gyazo.com/d8123c9f9a5fcfc48b2149c7faf48bad
Maybe try this, but I am not sure if this wont cause current icon to close down.
const [edit, setEdit] = useState(false)
const array = ['witch-king', 'sauron', 'azog']
const arrayrow = array && array.map(el=>{
return <div>
<i
className='fal fa-edit'
onClick={(e)=>{
setEdit(true);
e.stopPropagation()
}
></i>
<span>{el}</span>
</div>
})
useEffect(()=>{
if(edit){
setEdit(false)
}
},[edit])
Or try something like
const [edit, setEdit] = useState(false)
const array = ['witch-king', 'sauron', 'azog']
const arrayrow = array && array.map(el=>{
const checkEl = (e, currentEl) => {
if(el === currentEl){
e.stopPropagation()
setEdit(true)
}else{
setEdit(false)
}
}
return <div>
<i
className='fal fa-edit'
onClick={(e) => checkEl(e, el)}
></i>
<span>{el}</span>
</div>
})
useEffect(()=>{
document.addEventListener('click', ()=>{setEdit(false)})
},[])
Something like that.
FULL CODE SOLUTION
const MainComponent = () => {
const [currentEdit, setCurrentEdit] = useState('')
const array = ['witch-king', 'sauron', 'azog']
const arrayrow = array && array.map(el=>(
<ChildComponent
key={el}
el={el}
currentEdit={currentEdit}
handleClick={setCurrentEdit}>
)
)
return (<> {arrayrow} Some JSX </>)
}
const ChildComponent = ({ el, handleClick, currentEdit }) => {
return (
<div>
<i
className={`fal ${(currentEdit === el) ? 'fa-times' : 'fa-edit' }`}
onClick={(e) => {
handleClick(el)
}
></i>
<span>{el}</span>
</div>
)
}
EXPLANATION
Instead of using a boolean, I used the value of each item to represent the currently edited item, and initially, it is set to an empty string like this
const [currentEdit, setCurrentEdit] = useState('')
I made a child component passing each the currentEdit and the setCurrentEdit as props.
<ChildComponent
key={el}
el={el}
currentEdit={currentEdit}
handleClick={setCurrentEdit}>
)
Then do a check inside the child component and reset the currentEdit to that element on click
<i
className={`fal ${(currentEdit === el) ? 'fa-times' : 'fa-edit' }`}
onClick={(e) => {
handleClick(el)
}
></i>
PS: Remove the useEffect with the event listener, adding event listeners like that is an anti-pattern in React, and also you don't need it with this solution.

All buttons are clicked at the same time instead of the specific one clicked

I am much confused as I don't know what I am doing wrong. Each time I clicked on the plus sign, all the other div elements display instead of the specific one I click on. I tried to use id argument in my show and hide functions, it is complaining of too many re-rendering . I have been on this for the past 12 hours. I need your help to solving this mystery. All I want to do is to click on the plus sign to display only the content and minus sign to hide it.
import React, {useState, useEffect} from 'react'
function Home() {
const [userData, setUserData] = useState([]);
const [showing, setShowing] = useState(false)
const [search, setSearch] = useState("");
const [clicked, setClicked] = useState("")
async function getData()
{
let response = await fetch('https://api.hatchways.io/assessment/students');
let data = await response.json();
return data;
}
useEffect(() => {
getData()
.then(
data => {
setUserData(data.students ) }
)
.catch(error => {
console.log(error);
})
}, [])
const handleFilterChange = e => {
setSearch(e.target.value)
}
function DataSearch(rows) {
const columns = rows[0] && Object.keys(rows[0]);
return rows.filter((row) =>
columns.some((column) => row[column].toString().toLowerCase().indexOf(search.toLowerCase()) > -1)
);
}
const searchPosts = DataSearch(userData);
const show = (id, e) => {
setShowing(true);
}
const hide = (id, e) => {
setShowing(false);
}
return (
<>
<div>
<input value={search} onChange={handleFilterChange} placeholder={"Search by name"} />
</div>
{
searchPosts.map((student) => (
<div key={student.id} className="holder">
<div className="images">
<img src={student.pic} alt="avatar" width="130" height="130" />
</div>
<div className="data-container">
<span className="name">{student.firstName.toUpperCase()} {student.lastName.toUpperCase()}</span>
<span>Email: {student.email}</span>
<span></span>
<span>Company: {student.company}</span>
<span>Skill: {student.skill}</span>
<span>City: {student.city}</span>
{ showing ?
<button id={student.id} onClick={hide}>-</button>
: <button id={student.id} onClick={show}>+</button>
}
<div data-id={student.id}>
{ (showing )
? student.grades.map((grade, index) => (
<span id={index} key={index}>Test {index}: {grade}%</span>
)) : <span>
</span>
}
</div>
</div>
</div>
))
}
</>
)
}
export default Home
Change,
const [showing, setShowing] = useState(false)
to:
const [showing, setShowing] = useState({});
Here change the useState from boolean to object.. Reason for this is we will store the ids as keys and a boolean value indicating if the grade should be shown or not.
And remove Show and hide function and have a common toggle function like,
const toggleGrades = (id) => {
setShowing((previousState) => ({
...previousState,
[id]: !previousState[id]
}));
};
You are using setShowing(true) in show function and setShowing(false) in hide function which is the reason for opening all and closing all at any click.. Because you have never mentioned which exact grade should be shown so you need to make use of id here..
And buttons click handler will be like,
{showing[student.id] ? (
<button id={student.id} onClick={() => toggleGrades(student.id)}>
-
</button>
) : (
<button id={student.id} onClick={() => toggleGrades(student.id)}>
+
</button>
)}
So pass student id () => toggleGrades(student.id) in both show and hide button an make the button gets toggled.
Display the grades like,
<div data-id={student.id}>
{showing[student.id] ? (
student.grades.map((grade, index) => (
<span id={index} key={index}>
Test {index}: {grade}%
</span>
))
) : (
<span></span>
)}
</div>
Here if showing[student.id] will display only the grades of clicked item.
And that is why id plays a major role in such case.
Working Example:

updating HTML in react/gatsby

I'm trying to re render my HTML after I have clicked on a badge:
onClick={() => filterCategories(c.category)}
The code:
const BlogPage = props => {
console.log(props);
//After I click on the button comes back with the correct posts.
let posts =
props.data.allContentfulPost !== undefined
? props.data.allContentfulPost.edges
: props.data;
const categoriesStyle = {
marginBottom: '8px',
};
const filterCategories = category => {
posts = posts.filter(p => p.node.category === category);
BlogPage({ data: posts });
};
return(
<span tabIndex="0" key={c.id}
onClick={() => filterCategories(c.category)}
role="button"
onKeyDown={filterCategories}>
<Badge value={c.category} category={c.category} color="#fff">
{c.category}
</Badge>
</span>
)
}
So if I'm not mistaking I have to make use of setState, but when I try to use it I can't do it because is not a class and I will need a class to add the constructor as well right? The problem with this code is that it was writing by someone else using Gatsby and I'm new to React and Gatsby
I'd recommend you maintain the category in state and then filter posts based on that stateful category.
const BlogPage = props => {
const [category, setCategory] = useState(null);
const posts =
props.data.allContentfulPost !== undefined
? props.data.allContentfulPost.edges
: props.data;
const categoriesStyle = {
marginBottom: '8px',
};
const filterCategories = category => {
setCategory(category)
};
// These are your filtered posts
const filteredPosts = category ?
posts.filter(p => p.node.category === category) :
posts;
return(
<span tabIndex="0" key={c.id}
onClick={() => filterCategories(c.category)}
role="button"
onKeyDown={filterCategories}>
<Badge value={c.category} category={c.category} color="#fff">
{c.category}
</Badge>
</span>
)
}

Resources