I have two categories "A" and "B". On Click any button of category "A" removes the button and must move to category "B", On Click any button of category "B" adds the button to category "A" and must move from category "B".
export const LauncherButtons = [
{
linked: false,
type: 'ABC',
name: 'ReactJs'
},
{
linked: false,
type: 'ABC',
name: 'VueJS'
},
{
linked: true,
type: 'XYZ',
name: 'Angular'
},
{
linked: true,
type: 'XYZ',
name: 'Javascript'
}
];
This is what I am rendering for category "A".
{ LauncherButtons.map((button,index) => {
return(
button.linked === true &&
<LauncherActionButton
text={button.name}
onClick = {this.removeAction}/>
);
})}
Rendering category "B".
{ LauncherButtons.map((button,index) => {
return(
button.linked !== true &&
<LauncherActionButtonAdd
textAdd={button.name}
onClick = {this.addAction}/>
);
})}
So basically, when I click on a button of category "A" (True) it should move to category "B" and become false, similarly, when I click on a button of category "B" (False) it should become true and move to category "A".
Try something like this: https://codesandbox.io/s/holy-leftpad-hw1oe
I've laid out two sections, an active and inactive section. By clicking on a button, you move it to the opposite side. I don't know what your LauncherActionButton component looks like so consider this like a bare-bones template.
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
export const LauncherButtons = [
{
linked: false,
type: "ABC",
name: "ReactJs"
},
{
linked: false,
type: "ABC",
name: "VueJS"
},
{
linked: true,
type: "XYZ",
name: "Angular"
},
{
linked: true,
type: "XYZ",
name: "Javascript"
}
];
class App extends React.Component {
state = {
buttons: LauncherButtons
};
createActiveButtons = () => {
const { buttons } = this.state;
return buttons
.filter(button => {
return button.linked;
})
.map(activeButton => {
return (
<button onClick={this.handleOnClick} name={activeButton.name}>
{activeButton.name}
</button>
);
});
};
createInactiveButtons = () => {
const { buttons } = this.state;
return buttons
.filter(button => {
return !button.linked;
})
.map(inactiveButton => {
return (
<button onClick={this.handleOnClick} name={inactiveButton.name}>
{inactiveButton.name}
</button>
);
});
};
handleOnClick = event => {
const { buttons } = this.state;
const { name } = event.target;
let updatedButtons = buttons.map(button => {
if (button.name === name) {
return {
...button,
linked: !button.linked
};
} else {
return button;
}
});
this.setState({
buttons: updatedButtons
});
};
render() {
return (
<div style={{ display: "flex" }}>
<div style={{ width: "50%", background: "green", height: "300px" }}>
{this.createActiveButtons()}
</div>
<div style={{ width: "50%", background: "red", height: "300px" }}>
{this.createInactiveButtons()}
</div>
</div>
);
}
}
What about using the item as a parameter? for example:
removeAction(button) {
// change button however you like
}
// in the render method
{
LauncherButtons.map((button, index) => {
return (
button.linked &&
<LauncherActionButton
text={button.name}
onClick={() => removeAction(button)}/>
);
})
}
Related
I'm currently stuck on trying to display the number of correct answers once the quiz is finished.
Basically, I have created a state that keeps track of the number of correct answers shown within the QuizItem component. If the user selected answer matches the correct answer, then the user selected answer turns to green and it will increase the state of correctCount (as seen in the code) to 1. This new value is then passed to the parent component of QuizItem which is QuizList.
/* eslint-disable react/prop-types */
import React from "react";
import AnswerButton from "../UI/AnswerButton";
import classes from "./QuizItem.module.css";
export default function QuizItem(props) {
const [correctCount, setCorrectCount] = React.useState(0)
function addToCorrectCount() {
setCorrectCount(correctCount + 1)
}
props.onSaveCorrectCountData(correctCount)
console.log(correctCount);
return (
<div>
<div key={props.id} className={classes.quizlist__quizitem}>
<h3 className={classes.quizitem__h3}>{props.question}</h3>
{props.choices.map((choice) => {
const styles = {
backgroundColor: choice.isSelected ? "#D6DBF5" : "white",
};
// React.useEffect(() => {
// if (choice.isSelected && choice.choice === choice.correct) {
// addToCorrectCount();
// }
// }, [choice.isSelected, choice.correct]);
function checkAnswerStyle() {
/* this is to indicate that the selected answer is right, makes button go green*/
if (choice.isSelected && choice.choice === choice.correct) {
addToCorrectCount()
return {
backgroundColor: "#94D7A2",
color: "#4D5B9E",
border: "none",
};
/* this is to indicate that the selected answer is wrong, makes button go red*/
} else if (choice.isSelected && choice.choice !== choice.correct) {
return {
backgroundColor: "#F8BCBC",
color: "#4D5B9E",
border: "none",
};
/* this is to highlight the right answer if a selected answer is wrong*/
} else if (choice.choice === choice.correct) {
return {
backgroundColor: "#94D7A2",
color: "#4D5B9E",
border: "none",
};
/* this is to grey out the incorrect answers*/
} else {
return {
color: "#bfc0c0",
border: "1px solid #bfc0c0",
backgroundColor: "white",
};
}
}
return (
<AnswerButton
key={choice.id}
onClick={() => {
props.holdAnswer(choice.id);
}}
style={props.endQuiz ? checkAnswerStyle() : styles}
>
{choice.choice}
</AnswerButton>
);
})}
</div>
</div>
);
}
// create a counter, and for every correct answer (green button), increase the counter by 1.
In the QuizList component, I have set another state to receive the incoming value from the QuizItem component and use this new value to display the number of correct answers once the check answers button has been clicked.
import React from "react";
import { nanoid } from "nanoid";
import QuizItem from "./QuizItem";
import Button from "../UI/Button";
import Card from "../UI/Card";
import classes from "./QuizList.module.css";
export default function QuizList(props) {
const [quiz, setQuiz] = React.useState([]);
const [endQuiz, setEndQuiz] = React.useState(false);
// const [newGame, setNewGame] = React.useState(false);
const [noOfCorrectAnswers, setNoOfCorrectAnswers] = React.useState()
function addCorrectCountHandler(correctCount) {
setNoOfCorrectAnswers(correctCount)
}
React.useEffect(() => {
/* This function turns HTML element entities into normal words */
function decodeHtml(html) {
const txt = document.createElement("textarea");
txt.innerHTML = html;
return txt.value;
}
fetch(
"https://opentdb.com/api.php?amount=5&category=9&difficulty=medium&type=multiple"
)
.then((res) => res.json())
.then((data) => {
const dataArray = data.results;
const newDataArray = dataArray.map((item) => {
return {
question: decodeHtml(item.question),
choices: [
{
choice: decodeHtml(item.correct_answer),
isSelected: false,
correct: decodeHtml(item.correct_answer),
id: nanoid(),
},
{
choice: decodeHtml(item.incorrect_answers[0]),
isSelected: false,
correct: decodeHtml(item.correct_answer),
id: nanoid(),
},
{
choice: decodeHtml(item.incorrect_answers[1]),
isSelected: false,
correct: decodeHtml(item.correct_answer),
id: nanoid(),
},
{
choice: decodeHtml(item.incorrect_answers[2]),
isSelected: false,
correct: decodeHtml(item.correct_answer),
id: nanoid(),
},
].sort(() => 0.5 - Math.random()),
id: nanoid(),
};
});
return setQuiz(newDataArray);
});
}, []);
// console.log(quiz);
function finishQuiz() {
setEndQuiz((prevEndQuiz) => !prevEndQuiz);
}
// function startNewGame() {
// setNewGame(true);
// }
function holdAnswer(quizId, choiceId) {
setQuiz((oldQuiz) =>
oldQuiz.map((quiz) => {
if (quiz.id !== quizId) return quiz;
return {
...quiz,
choices: quiz.choices.map((choice) =>
choice.id === choiceId
? // If the choice selected is the current choice, toggle its selected state
{ ...choice, isSelected: !choice.isSelected }
: // Otherwise, deselect the choice
{ ...choice, isSelected: false }
),
};
})
);
}
const quizItemComponents = quiz.map((item) => {
return (
<QuizItem
key={item.id}
question={item.question}
choices={item.choices}
holdAnswer={(id) => holdAnswer(item.id, id)}
endQuiz={endQuiz}
correct={quiz.correct}
onSaveCorrectCountData={addCorrectCountHandler}
/>
);
});
return (
<Card className={classes.quizlist}>
{quizItemComponents}
{!endQuiz && <Button onClick={finishQuiz}>Check Answers</Button>}
{endQuiz && (
<div className={classes.result}>
<p>You scored {noOfCorrectAnswers}/5 answers</p>
<Button onClick={startNewGame}>Play Again</Button>
</div>
)}
</Card>
);
}
The error that I was getting is that there were too many re-renders, so I tried using useEffect on the setCorrectCount state within my QuizItem component (this can be seen in my code and greyed out) but it would not tally up the count.
Is there a good workaround to this problem? Any help or advice would be appreciated.
Link to the code via Stackblitz:
https://stackblitz.com/edit/quizzical
I tried to add some optional costs for products, but I can't find a simpler way than this... I also don't know how to exactly formulate the question, so if anyone has an idea, I would gladly rename it... this is the code I want to simplify (i would like to automate the process for every item, for eg. do this for every item in array:
const extrasList = [
{name: extra1, price: 100, active: false},
{name: extra1, price: 100, active: false},
{name: extra1, price: 100, active: false}
]
):
1.Extras.jsx
import React, { useState } from "react";
import styles from "../styles/extras.module.css";
function Extras() {
const [extra1, setExtra1] = useState(false);
const extraItem1 = extra1 === true ? 20 : 0;
const [extra2, setExtra2] = useState(false);
const extraItem2 = extra2 === true ? 40 : 0;
const extrasPrice = extraItem1 + extraItem2;
return (
<div className={styles.extrasWrapper}>
<p className={extra1 === true ? styles.activeExtras : styles.extras} onClick={() => setExtra1(!extra1)}>Extra1 {extraItem1}</p>
<p className={extra2 === true ? styles.activeExtras : styles.extras} onClick={() => setExtra2(!extra2)}>Extra2 {extraItem2}</p>
</div>
);
}
export default Extras;
Extras.module.css
.activeExtras {
color: red;
}
.extras {
color: white;
}
.extrasWrapper {
display: flex;
gap: 0.2em;
}
You need to create a separate component and 1 state to maintain all extras and update them accordingly based on click. Assuming the name is unique. Check the components you can use.
function Extras(props) {
const [extrasList, setExtrasList] = useState(props.extrasList);
const toggleExtra = (extra) => {
const updatedExtrasList = extrasList.map((ex) => {
if (extra.name === ex.name) {
ex.active = !ex.active;
}
return ex;
});
setExtrasList(updatedExtrasList);
};
return (
<div className={styles.extrasWrapper}>
{extrasList.map((extra) => {
return (
<p
key={extra.name}
className={extra.active ? "activeExtras" : "extras"}
onClick={() => toggleExtra(extra)}
>
{extra.name} {extra.active ? extra.price : 0}
</p>
);
})}
</div>
);
}
From your parent component pass extrasList props.
const extrasList = [
{ name: "Extra1", price: 100, active: false },
{ name: "Extra2", price: 200, active: false },
{ name: "Extra3", price: 300, active: false }
];
return (
<div className="App">
<Extras extrasList={extrasList} />
</div>
);
Working sample https://codesandbox.io/s/lucid-http-3iy5d8
I am trying to create an ObjectList component, which would contain a list of Children.
const MyList = ({childObjects}) => {
[objects, setObjects] = useState(childObjects)
...
return (
<div>
{childObjects.map((obj, idx) => (
<ListChild
obj={obj}
key={idx}
collapsed={false}
/>
))}
</div>
)
}
export default MyList
Each Child has a collapsed property, which toggles its visibility. I am trying to have a Collapse All button on a parent level which will toggle the collapsed property of all of its children. However, it must only change their prop once, without binding them all to the same state. I was thinking of having a list of refs, one for each child and to enumerate over it, but not sure if it is a sound idea from design perspective.
How can I reference a dynamic list of child components and manage their state?
Alternatively, is there a better approach to my problem?
I am new to react, probably there is a better way, but the code below does what you explained, I used only 1 state to control all the objects and another state to control if all are collapsed.
Index.jsx
import MyList from "./MyList";
function Index() {
const objList = [
{ data: "Obj 1", id: 1, collapsed: false },
{ data: "Obj 2", id: 2, collapsed: false },
{ data: "Obj 3", id: 3, collapsed: false },
{ data: "Obj 4", id: 4, collapsed: false },
{ data: "Obj 5", id: 5, collapsed: false },
{ data: "Obj 6", id: 6, collapsed: false },
];
return <MyList childObjects={objList}></MyList>;
}
export default Index;
MyList.jsx
import { useState } from "react";
import ListChild from "./ListChild";
const MyList = ({ childObjects }) => {
const [objects, setObjects] = useState(childObjects);
const [allCollapsed, setallCollapsed] = useState(false);
const handleCollapseAll = () => {
allCollapsed = !allCollapsed;
for (const obj of objects) {
obj.collapsed = allCollapsed;
}
setallCollapsed(allCollapsed);
setObjects([...objects]);
};
return (
<div>
<button onClick={handleCollapseAll}>Collapse All</button>
<br />
<br />
{objects.map((obj) => {
return (
<ListChild
obj={obj.data}
id={obj.id}
key={obj.id}
collapsed={obj.collapsed}
state={objects}
setState={setObjects}
/>
);
})}
</div>
);
};
export default MyList;
ListChild.jsx
function ListChild(props) {
const { obj, id, collapsed, state, setState } = props;
const handleCollapse = (id) => {
console.log("ID", id);
for (const obj of state) {
if (obj.id == id) {
obj.collapsed = !obj.collapsed;
}
}
setState([...state]);
};
return (
<div>
{obj} {collapsed ? "COLLAPSED!" : ""}
<button
onClick={() => {
handleCollapse(id);
}}
>
Collapse This
</button>
</div>
);
}
export default ListChild;
I'm trying to build a menu bar where the user can hover over the options and the corresponding sub-menu shows.
I have implemented this behaviour using OnMouseEnter and OnMouseLeave. Everything works ok if I enter an item and then exit it without entering another but the problem arises when I move from one item to another directly. It seems that OnMouseLeave is not fully executed before calling OnMouseEnter on the new Item and this leads to the state not being correctly updated (this is a guess though).
Here is the code:
import React from 'react';
import { useEffect } from 'react';
import { useState } from 'react';
import { BsChevronDown } from 'react-icons/bs';
import IMenuItemData from './Menu/IMenuItemData';
import NavigationMenu from './Menu/NavigationMenu';
function NavigationBar({menuData}:{menuData:IMenuItemData[]}) {
const [hoverStates, setHovering] = useState<any>({});
function _onMouseEnter(id:string|undefined)
{
console.log("OnMouseEnter id -> "+id);
console.log(hoverStates);
if(id)
{
let newState:any = {...hoverStates};
newState[id] = true;
setHovering(newState);
}
}
function _onMouseLeave(id:string|undefined)
{
console.log("OnMouseLeave id -> "+id);
console.log(hoverStates);
if(id)
{
let newState:any = {...hoverStates};
newState[id] = false;
setHovering(newState);
}
}
useEffect(()=>{
let states: any = {};
menuData.forEach( (el:IMenuItemData) =>{
if(el.id) states[el.id] = false;
});
setHovering(states);
},[]);
return (
<nav className="flex">
{menuData.map((e,index) => {
return<div key={index}>
<a href="#" onMouseEnter={()=>_onMouseEnter(e.id)} onMouseLeave={()=>_onMouseLeave(e.id)} className="flex p-2 items-center bg-red-400">
{e.text}
<BsChevronDown className="ml-2"></BsChevronDown>
</a>
{e.children && e.id ? <NavigationMenu data={e.children} visible={hoverStates[e.id]}></NavigationMenu> : null}
</div>
})}
</nav>
)
}
export default NavigationBar;
export default interface IMenuItemData
{
text: string,
children? : IMenuItemData[] ,
id?: string
}
This just iterates through IMenuItemData objects and add them to the menu and adds a key to the state to track the hovering for every menu item.
This is the output when just entering an element and exiting without entering a new one:
OnMouseEnter id -> menu-item-store NavigationBar.tsx:15
Object { "menu-item-store": false, "menu-item-about": false, "menu-item-community": true, "menu-item-support": false }
NavigationBar.tsx:16
OnMouseLeave id -> menu-item-store NavigationBar.tsx:27
Object { "menu-item-store": true, "menu-item-about": false, "menu-item-community": true, "menu-item-support": false }
NavigationBar.tsx:28
And this the output that is logged when I leave a menu option but enters another immediately:
OnMouseEnter id -> menu-item-store NavigationBar.tsx:15
Object { "menu-item-store": false, "menu-item-about": false, "menu-item-community": true, "menu-item-support": false }
NavigationBar.tsx:16
OnMouseLeave id -> menu-item-store NavigationBar.tsx:27
Object { "menu-item-store": true, "menu-item-about": false, "menu-item-community": true, "menu-item-support": false }
NavigationBar.tsx:28
OnMouseEnter id -> menu-item-about NavigationBar.tsx:15
Object { "menu-item-store": true, "menu-item-about": false, "menu-item-community": true, "menu-item-support": false }
NavigationBar.tsx:16 <--- This should show 'menu-item-store': false
OnMouseLeave id -> menu-item-about NavigationBar.tsx:27
Object { "menu-item-store": true, "menu-item-about": true, "menu-item-community": true, "menu-item-support": false }
NavigationBar.tsx:28
A simple solution to keep visibility control in the nav component is to use an active flag. Adapted Keith's snippet below.
(Also, don't use index as key, use a unique property of the mapped element instead.)
const { useState } = React;
const mount = document.querySelector('#mount');
function Menu() {
const [menuItems, setMenuItems] = useState([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, { id: 6 }]);
const [active, setActive] = useState(undefined);
return (
<div>
{
menuItems.map(item => (
<div
key={item.id}
onMouseEnter={() => setActive(item.id)}
onMouseLeave={() => setActive(undefined)}
style={{
display: "inline-block",
margin: "2px",
width: "40px",
height: "40px",
backgroundColor: active === item.id? "pink": "yellow",
}}
></div>
))}
</div>
)
}
ReactDOM.render(<Menu />, mount);
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="mount"></div>
Managing the visibility/active state inside each menuItem as per your question is possible, but verbose and may lead to clashes of state. (note that I am using a callback in the setMenuItems() call to access the most current previous value.)
const { useState } = React;
function Menu() {
const [menuItems, setMenuItems] = useState([{ id: 1, visible: false }, { id: 2, visible: false }, { id: 3, visible: false }, { id: 4, visible: false }, { id: 5, visible: false }, { id: 6, visible: false }]);
const handleOnMouseEnter = (id) => {
const itemIndex = menuItems.findIndex(({ id: id_ }) => id_ === id);
setMenuItems(prevMenuItems => [
...prevMenuItems.slice(0, itemIndex),
{ ...prevMenuItems[itemIndex], visible: true },
...prevMenuItems.slice(itemIndex + 1)])
}
const handleOnMouseLeave = (id) => {
const itemIndex = menuItems.findIndex(({ id: id_ }) => id_ === id);
setMenuItems(prevMenuItems => [
...prevMenuItems.slice(0, itemIndex),
{ ...prevMenuItems[itemIndex], visible: false },
...prevMenuItems.slice(itemIndex + 1)])
}
return (
<div>
{
menuItems.map(item => (
<div
key={item.id}
onMouseEnter={() => handleOnMouseEnter(item.id)}
onMouseLeave={() => handleOnMouseLeave(item.id)}
style={{
display: "inline-block",
margin: "2px",
width: "40px",
height: "40px",
backgroundColor: item.visible ? 'pink' : 'yellow',
}}
></div>
))}
</div>
)
}
ReactDOM.render(<Menu />, document.querySelector('#mount'));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="mount"></div>
I'm not able to reproduce the issue.
Below is a simple working snippet, you could maybe tweak to show the issue.
const {useState} = React;
const mount = document.querySelector('#mount');
function HoverBox() {
const [color, setColor] = useState('red');
return <div
onMouseEnter={() => setColor('green')}
onMouseLeave={() => setColor('red')}
style={{
display: "inline-block",
margin: "2px",
width: "40px",
height: "40px",
backgroundColor: color
}}
></div>;
}
ReactDOM.render(<div>
<HoverBox/><HoverBox/><HoverBox/>
<HoverBox/><HoverBox/><HoverBox/>
</div>, mount);
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="mount"></div>
Print selected radio button value in console.
If all radiogroup is answered then print in console only the selected= true radioValue. for example: if NO radiobutton= true its value is 2. It should print value 2. Like that all true radiovalue should print in console.
Thanks
//array of cards coming from the backend
const data = [
{
cardName: 'Do you want sugar in your coffee',
options: [
{ radioName: 'Yes',radioValue: '1', selected: false },
{ radioName: 'No',radioValue: '2', selected: false }]
},
{
cardName: 'Do you want milk in your coffee',
options: [
{ radioName: 'Yes',radioValue: '1', selected: false },
{ radioName: 'No',radioValue: '2', selected: false }]
},
{
cardName: 'Do you want low-fat-milk in your coffee',
options: [
{ radioName: 'Yes',radioValue: '1', selected: false },
{ radioName: 'No',radioValue: '2', selected: false }]
}
];
class CardsList extends React.Component {
constructor(props) {
super(props);
this.state = {
cards: [],
};
}
componentDidMount() {
setTimeout(() => {
// mimic an async server call
this.setState({ cards: data });
}, 1000);
}
onInputChange = ({ target }) => {
const { cards } = this.state;
const nexState = cards.map(card => {
if (card.cardName !== target.name) return card;
return {
...card,
options: card.options.map(opt => {
const checked = opt.radioName === target.value;
return {
...opt,
selected: checked
}
})
}
});
this.setState({ cards: nexState })
}
onSubmit = () => {
console.log(this.state.cards.map(({ cardName, options }) => {
const option = options.filter(({ selected }) => selected)[0]
return ` ${option.radioValue}`
}))
};
onReset = () => {
this.setState({cards:[]});
}
render() {
const { cards } = this.state;
return (
<div>
{
cards.length < 1 ? "Loading..." :
<div>
{cards.map((card, idx) => (
<ul>
{card.cardName}
{card.options.radioName}
{
card.options.map((lo, idx) => {
return <input
key={idx}
type="radio"
name={card.cardName}
value={lo.radioName}
checked={!!lo.selected}
onChange={this.onInputChange}
/>
})
}
</ul>
))
}
< button onClick={this.onSubmit}>Submit</button>
< button onClick={this.onReset}>Clear</button>
</div>
}
</div>
);
}
}
ReactDOM.render(<CardsList />, document.getElementById('root'));
<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="root"></div>
Change your log in onSubmit to this
console.log(this.state.cards.map(({ cardName, options }) => {
const option = options.filter(({ selected }) => selected)[0]
return `${cardName}: ${option.radioName}`
}))
This way you filter the options to the one, where selected is truthy, and take the first one.
To address your first question, just map over your this.state.cards array before doing the log and check, if there is exactly 1 option, where selected is true. If this is not the case, tell the user in whatever way you want.
Also you can remove your constructor and change it to that:
state = {
cards: [],
}
Because you do not access your props in your constructor
You can go with the answer of #george,
for you to check if either of the radio buttons is clicked for each card, you can run a validation check
let unselectedCards = this.state.cards.filter((card) => {
return !card.options[0].selected && !card.options[1].selected
});
use the unselectedCards variable to highlight the cards.
you can use map options again inside the cards map if you would be having more options.