React - how to access other elements in handleClick? - javascript

I am iterating over an array, and for each element, I pass it to a handleClick function. The question is, inside that handleClick function, how do I access the rest of the elements?
const listOfAnswers = questions[questionNumber].possibleAnswers.map((obj, i, arr) => {
return (
<Button
key={i}
style={
{
margin: '15px 0',
}
}
variant='contained'
onClick={e => handleClick(obj, e, arr)}
>
{obj.answer}
</Button>
)
})
const handleClick = async (obj, e, arr) => {
const { isCorrect, answer } = obj
if (isCorrect) {
setScore(score + 1)
e.target.style.backgroundColor = 'green'
await delay(100)
e.target.style.backgroundColor = ''
} else {
e.target.style.backgroundColor = 'red'
await delay(100)
e.target.style.backgroundColor = ''
}
nextQuestion()
}
What I am trying to do is: when a user clicks on the right answer, that button turns green. This is straightforward to implement. When a user clicks the wrong answer, it turns red. Also simple to implement. But what I want is: when a user clicks on the wrong answer, I want the right answer to turn green. For this I think I need to be able to access the rest of the elements, because in the handleClick function, you only have access to a single, individual element.

const [clicked, setClicked] = useState(-1);
const listOfAnswers = questions[questionNumber].possibleAnswers.map((obj, i, arr) => {
return (
<Button
key={i}
style={
{
margin: '15px 0',
color: clicked == -1
? "#ffffff"
: clicked == i && obj.isCorrect
? "#00ff00"
: cliked != i
? "#ffffff"
: "#ff0000"
}
}
variant='contained'
onClick={e => handleClick(i)}
>
{obj.answer}
</Button>
)
})
const handleClick = async (i) => {
setClicked(i)
nextQuestion()
}

const [clicked, setClicked] = useState(-1);
const listOfAnswers = questions[questionNumber].possibleAnswers.map((obj, i, arr) => {
return (
<Button
key={i}
style={
{
margin: '15px 0',
color: clicked == -1
? "#ffffff"
: clicked == i && obj.isCorrect
? "#00ff00"
: cliked != i
? "#ffffff"
: "#ff0000"
}
}
variant='contained'
onClick={handleClick}
>
{obj.answer}
</Button>
)
})
const handleClick = (e) => {
setClicked(e.target.key)
}

There are various way to do it:
Add a answer to the state (initialized to undefined). Your handleClick will set this state to the answer the user selected. Then if answer is defined, pass a green backgroundColor in the style of the correct button (next to your margin). And on the button whose obj.answer === answer, if obj.isCorrect is false, set a red backgroundColor. (note you would need to reset answer state to undefined in your nextQuestion)
Add a ref to the correct answer Button (https://reactjs.org/docs/refs-and-the-dom.html), and on handleClick, you can set ref.current.style.background.
UPDATE with example of ref
The following assumes that there is a unique correct answer (link to codesandbox: https://codesandbox.io/s/hungry-swirles-7q6wz2?file=/src/App.js:0-2575):
import "./styles.css";
import { useRef, useState } from "react";
import { Button } from "#mui/material";
export default function App() {
return (
<div className="App">
<h1>Quiz time!</h1>
<Questions />
</div>
);
}
const Questions = () => {
const [score, setScore] = useState(0);
const [questionNumber, setQuestionNumber] = useState(0);
const correctAnswerRef = useRef(null);
// If all questions were answered we display the score
if (questionNumber >= questions.length) {
const reset = () => {
setScore(0);
setQuestionNumber(0);
};
return (
<div>
Score: {score} <Button onClick={reset}>Take the quiz again</Button>
</div>
);
}
const handleClick = async (obj, e) => {
const { isCorrect } = obj;
// always set current answer to green
if (correctAnswerRef.current) {
correctAnswerRef.current.style.backgroundColor = "green";
}
if (isCorrect) {
setScore(score + 1);
} else {
// if wrong answer was selected, put a red background
e.target.style.backgroundColor = "red";
}
// just to simulate `delay` since I don't have that util
await new Promise((resolve) => setTimeout(resolve, 1000));
e.target.style.backgroundColor = "";
if (correctAnswerRef.current) {
correctAnswerRef.current.style.backgroundColor = "";
}
// too lazy to implement nextQuestion, so I just increment the question number
setQuestionNumber(questionNumber + 1);
};
const question = questions[questionNumber];
const listOfAnswers = question.possibleAnswers.map((obj) => {
return (
<div>
<Button
key={obj.answer} // ideally each answer should have one (and you should never use index!)
ref={obj.isCorrect ? correctAnswerRef : undefined}
style={{
margin: "15px 0"
}}
variant="contained"
onClick={(e) => handleClick(obj, e)}
>
{obj.answer}
</Button>
</div>
);
});
return (
<div>
<div>{question.question}</div>
{listOfAnswers}
</div>
);
};
const questions = [
{
question: "Is this working?",
possibleAnswers: [
{
answer: "Yes",
isCorrect: true
},
{
answer: "No",
isCorrect: false
}
]
},
{
question: "Is this a good question?",
possibleAnswers: [
{
answer: "Nope",
isCorrect: false
},
{
answer: "Yes, it is!",
isCorrect: true
}
]
}
];

Related

Displaying number of correct answers for quiz app

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

Make other block disappear when chose multiple values

How can I make other filter button disappear when picked 1 (or multiple) value in same filter block.
Here is my code base:
const FilterBlock = props => {
const {
filterApi,
filterState,
filterFrontendInput,
group,
items,
name,
onApply,
initialOpen
} = props;
const { formatMessage } = useIntl();
const talonProps = useFilterBlock({
filterState,
items,
initialOpen
});
const { handleClick, isExpanded } = talonProps;
const classStyle = useStyle(defaultClasses, props.classes);
const ref = useRef(null);
useEffect(() => {
const handleClickOutside = event => {
if (ref.current && !ref.current.contains(event.target)) {
isExpanded && handleClick();
}
};
document.addEventListener('click', handleClickOutside, true);
return () => {
document.removeEventListener('click', handleClickOutside, true);
};
}, [isExpanded]);
const list = isExpanded ? (
<Form>
<FilterList
filterApi={filterApi}
filterState={filterState}
name={name}
filterFrontendInput={filterFrontendInput}
group={group}
items={items}
onApply={onApply}
/>
</Form>
) : null;
return (
<div
data-cy="FilterBlock-root"
aria-label={itemAriaLabel}
ref={ref}
>
<Menu.Button
data-cy="FilterBlock-triggerButton"
type="button"
onClick={handleClick}
aria-label={toggleItemOptionsAriaLabel}
>
<div>
<span>
{name}
</span>
<svg
width="8"
height="5"
viewBox="0 0 8 5"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.97291 0.193232C7.20854"
fill="currentColor"
/>
</svg>
</div>
</Menu.Button>
<div>
<div>
{list}
</div>
</div>
</div>
);
};
I am trying to achieve when I chose 1 value or more than 1 value inside filter block the other block will disappear but right now I achieved that when I chose 1 value the other filter block disappear but when I chose another value in the same block the other filter block appear again. Anyone have idea how can I work on this?
I am using React and Redux for this project
Thank you for helping me on this!!!!
Update:
Added parent component for FilterBlock.ks:
const FilterSidebar = props => {
const { filters, filterCountToOpen } = props;
const [selectedGroup, setSelectedGroup] = useState(null);
const talonProps = useFilterSidebar({ filters });
const {
filterApi,
filterItems,
filterNames,
filterFrontendInput,
filterState,
handleApply,
handleReset
} = talonProps;
const filterRef = useRef();
const classStyle = useStyle(defaultClasses, props.classes);
const handleApplyFilter = useCallback(
(...args) => {
const filterElement = filterRef.current;
if (
filterElement &&
typeof filterElement.getBoundingClientRect === 'function'
) {
const filterTop = filterElement.getBoundingClientRect().top;
const windowScrollY =
window.scrollY + filterTop - SCROLL_OFFSET;
window.scrollTo(0, windowScrollY);
}
handleApply(...args);
},
[handleApply, filterRef]
);
const result = Array.from(filterItems)
.filter(
([group, items]) =>
selectedGroup === null ||
selectedGroup === filterNames.get(group)
)
.map(([group, items], iteration) => {
const blockState = filterState.get(group);
const groupName = filterNames.get(group);
const frontendInput = filterFrontendInput.get(group);
return (
<FilterBlock
key={group}
filterApi={filterApi}
filterState={blockState}
filterFrontendInput={frontendInput}
group={group}
items={items}
name={groupName}
onApply={(...args) => {
console.log('args: ', ...args);
setSelectedGroup(prev =>
prev !== null ? null : groupName
);
return handleApplyFilter(...args);
}}
initialOpen={iteration < filterCountToOpen}
iteration={iteration}
/>
);
});
return (
<div className="container px-4 mx-auto">
<Menu
as="div"
className="my-16 justify-center flex flex-wrap py-5 border-y border-black border-opacity-5"
>
{result}
</Menu>
</div>
);
};
Updated added useFilterSideBar.js:
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from '#apollo/client';
import { useHistory, useLocation } from 'react-router-dom';
import { useAppContext } from '#magento/peregrine/lib/context/app';
import mergeOperations from '../../util/shallowMerge';
import { useFilterState } from '../FilterModal';
import {
getSearchFromState,
getStateFromSearch,
sortFiltersArray,
stripHtml
} from '../FilterModal/helpers';
import DEFAULT_OPERATIONS from '../FilterModal/filterModal.gql';
const DRAWER_NAME = 'filter';
export const useFilterSidebar = props => {
const { filters } = props;
const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
const { getFilterInputsQuery } = operations;
const [isApplying, setIsApplying] = useState(false);
const [{ drawer }, { toggleDrawer, closeDrawer }] = useAppContext();
const [filterState, filterApi] = useFilterState();
const prevDrawer = useRef(null);
const isOpen = drawer === DRAWER_NAME;
const history = useHistory();
const { pathname, search } = useLocation();
const { data: introspectionData } = useQuery(getFilterInputsQuery);
const attributeCodes = useMemo(
() => filters.map(({ attribute_code }) => attribute_code),
[filters]
);
// Create a set of disabled filters.
const DISABLED_FILTERS = useMemo(() => {
const disabled = new Set();
// Disable category filtering when not on a search page.
if (pathname !== '/search.html') {
disabled.add('category_id');
disabled.add('category_uid');
}
return disabled;
}, [pathname]);
// Get "allowed" filters by intersection of filter attribute codes and
// schema input field types. This restricts the displayed filters to those
// that the api will understand.
const possibleFilters = useMemo(() => {
const nextFilters = new Set();
const inputFields = introspectionData
? introspectionData.__type.inputFields
: [];
// perform mapping and filtering in the same cycle
for (const { name } of inputFields) {
const isValid = attributeCodes.includes(name);
const isEnabled = !DISABLED_FILTERS.has(name);
if (isValid && isEnabled) {
nextFilters.add(name);
}
}
return nextFilters;
}, [DISABLED_FILTERS, attributeCodes, introspectionData]);
const isBooleanFilter = options => {
const optionsString = JSON.stringify(options);
return (
options.length <= 2 &&
(optionsString.includes(
JSON.stringify({
__typename: 'AggregationOption',
label: '0',
value: '0'
})
) ||
optionsString.includes(
JSON.stringify({
__typename: 'AggregationOption',
label: '1',
value: '1'
})
))
);
};
// iterate over filters once to set up all the collections we need
const [
filterNames,
filterKeys,
filterItems,
filterFrontendInput
] = useMemo(() => {
const names = new Map();
const keys = new Set();
const frontendInput = new Map();
const itemsByGroup = new Map();
const sortedFilters = sortFiltersArray([...filters]);
for (const filter of sortedFilters) {
const { options, label: name, attribute_code: group } = filter;
// If this aggregation is not a possible filter, just back out.
if (possibleFilters.has(group)) {
const items = [];
// add filter name
names.set(group, name);
// add filter key permutations
keys.add(`${group}[filter]`);
// TODO: Get all frontend input type from gql if other filter input types are needed
// See https://github.com/magento-commerce/magento2-pwa/pull/26
if (isBooleanFilter(options)) {
frontendInput.set(group, 'boolean');
// add items
items.push({
title: 'No',
value: '0',
label: name + ':' + 'No'
});
items.push({
title: 'Yes',
value: '1',
label: name + ':' + 'Yes'
});
} else {
// Add frontend input type
frontendInput.set(group, null);
// add items
for (const { label, value } of options) {
items.push({ title: stripHtml(label), value });
}
}
itemsByGroup.set(group, items);
}
}
return [names, keys, itemsByGroup, frontendInput];
}, [filters, possibleFilters]);
// on apply, write filter state to location
useEffect(() => {
if (isApplying) {
const nextSearch = getSearchFromState(
search,
filterKeys,
filterState
);
// write filter state to history
history.push({ pathname, search: nextSearch });
// mark the operation as complete
setIsApplying(false);
}
}, [filterKeys, filterState, history, isApplying, pathname, search]);
const handleOpen = useCallback(() => {
toggleDrawer(DRAWER_NAME);
}, [toggleDrawer]);
const handleClose = useCallback(() => {
closeDrawer();
}, [closeDrawer]);
const handleApply = useCallback(() => {
setIsApplying(true);
handleClose();
}, [handleClose]);
const handleReset = useCallback(() => {
filterApi.clear();
setIsApplying(true);
}, [filterApi, setIsApplying]);
const handleKeyDownActions = useCallback(
event => {
// do not handle keyboard actions when the modal is closed
if (!isOpen) {
return;
}
switch (event.keyCode) {
// when "Esc" key fired -> close the modal
case 27:
handleClose();
break;
}
},
[isOpen, handleClose]
);
useEffect(() => {
const justOpened =
prevDrawer.current === null && drawer === DRAWER_NAME;
const justClosed =
prevDrawer.current === DRAWER_NAME && drawer === null;
// on drawer toggle, read filter state from location
if (justOpened || justClosed) {
const nextState = getStateFromSearch(
search,
filterKeys,
filterItems
);
filterApi.setItems(nextState);
}
// on drawer close, update the modal visibility state
if (justClosed) {
handleClose();
}
prevDrawer.current = drawer;
}, [drawer, filterApi, filterItems, filterKeys, search, handleClose]);
useEffect(() => {
const nextState = getStateFromSearch(search, filterKeys, filterItems);
filterApi.setItems(nextState);
}, [filterApi, filterItems, filterKeys, search]);
return {
filterApi,
filterItems,
filterKeys,
filterNames,
filterFrontendInput,
filterState,
handleApply,
handleClose,
handleKeyDownActions,
handleOpen,
handleReset,
isApplying,
isOpen
};
};
Update FilterList component:
const FilterList = props => {
const {
filterApi,
filterState,
filterFrontendInput,
name,
group,
itemCountToShow,
items,
onApply,
toggleItemOptionsAriaLabel
} = props;
const classes = useStyle(defaultClasses, props.classes);
const talonProps = useFilterList({ filterState, items, itemCountToShow });
const { isListExpanded, handleListToggle } = talonProps;
const { formatMessage } = useIntl();
// memoize item creation
// search value is not referenced, so this array is stable
const itemElements = useMemo(() => {
if (filterFrontendInput === 'boolean') {
const key = `item-${group}`;
return (
<li
key={key}
className={classes.item}
data-cy="FilterList-item"
>
<FilterItemRadioGroup
filterApi={filterApi}
filterState={filterState}
group={group}
name={name}
items={items}
onApply={onApply}
labels={labels}
/>
</li>
);
}
return items.map((item, index) => {
const { title, value } = item;
const key = `item-${group}-${value}`;
if (!isListExpanded && index >= itemCountToShow) {
return null;
}
// create an element for each item
const element = (
<li
key={key}
className={classes.item}
data-cy="FilterList-item"
>
<FilterItem
filterApi={filterApi}
filterState={filterState}
group={group}
item={item}
onApply={onApply}
/>
</li>
);
// associate each element with its normalized title
// titles are not unique, so use the element as the key
labels.set(element, title.toUpperCase());
return element;
});
}, [
classes,
filterApi,
filterState,
filterFrontendInput,
name,
group,
items,
isListExpanded,
itemCountToShow,
onApply
]);
const showMoreLessItem = useMemo(() => {
if (items.length <= itemCountToShow) {
return null;
}
const label = isListExpanded
? formatMessage({
id: 'filterList.showLess',
defaultMessage: 'Show Less'
})
: formatMessage({
id: 'filterList.showMore',
defaultMessage: 'Show More'
});
return (
<li className={classes.showMoreLessItem}>
<button
onClick={handleListToggle}
className="text-sm hover_text-indigo-500 transition-colors duration-sm"
data-cy="FilterList-showMoreLessButton"
>
{label}
</button>
</li>
);
}, [
isListExpanded,
handleListToggle,
items,
itemCountToShow,
formatMessage,
classes
]);
return (
<Fragment>
<ul className={classes.items}>
{itemElements}
{showMoreLessItem}
</ul>
</Fragment>
);
};
FilterList.defaultProps = {
onApply: null,
itemCountToShow: 5
};
Update FilterRadioGroup:
const FilterItemRadioGroup = props => {
const { filterApi, filterState, group, items, onApply, labels } = props;
const radioItems = useMemo(() => {
return items.map(item => {
const code = `item-${group}-${item.value}`;
return (
<FilterItemRadio
key={code}
filterApi={filterApi}
filterState={filterState}
group={group}
item={item}
onApply={onApply}
labels={labels}
/>
);
});
}, [filterApi, filterState, group, items, labels, onApply]);
const fieldValue = useMemo(() => {
if (filterState) {
for (const item of items) {
if (filterState.has(item)) {
return item.value;
}
}
}
return null;
}, [filterState, items]);
const field = `item-${group}`;
const fieldApi = useFieldApi(field);
const fieldState = useFieldState(field);
useEffect(() => {
if (field && fieldValue === null) {
fieldApi.reset();
} else if (field && fieldValue !== fieldState.value) {
fieldApi.setValue(fieldValue);
}
}, [field, fieldApi, fieldState.value, fieldValue]);
return (
<RadioGroup field={field} data-cy="FilterDefault-radioGroup">
{radioItems}
</RadioGroup>
);
};
FilterItemRadioGroup.defaultProps = {
onApply: null
};
The code seems overly complicated for what it does.
Could you perhaps make the filter block a state object like this:
filterGroup = {
title: "",
hasSelectedItems: boolean,
filterItems: []
}
Then it would be a simple matter of checking hasSelectedItems to conditionally render only one filterGroup in the UI.
I apologize I don't have the time to go in depth and try out your code, but I would put the filterBlock rendering inside the return statement and do something like this:
return(
{ (!!filterValue && (filterGroup === selectedGroup))
&&
<FilterGroup />
}
);
I find the best results come from using my state values as conditions inside my return statement.
That way, I don't have to manage complex logic in the .map() functions or with useEffect.
It looks like FilterSideBar is where you build the the array of FilterBlocks.
According to your question, I think you want to display all filter blocks if NO filters are selected, but once a filter is selected, you want the other FilterBlocks to not render, correct?
So, in your FilterSidebar module, I'd modify your "result" variable to include a check to see if anything has already been selected.
If something has been selected, then only render the FilterBlock that corresponds to that selected list item.
const result = Array.from(filterItems)
.filter(
([group, items]) =>
selectedGroup === null ||
selectedGroup === filterNames.get(group)
)
.map(([group, items], iteration) => {
const blockState = filterState.get(group);
const groupName = filterNames.get(group);
const frontendInput = filterFrontendInput.get(group);
return (
// This is the conditional rendering
(!!blockState.filterGroupSelected && blockState.filterGroupSelected === group) ?
<FilterBlock
key={group}
...
/>
: null
);
});
Then you'll have to add an item to your state object (unless you already have it) to track the group that was selected (e.g. "Printer Type"), and only allow that one to render.
If no group has a selected filter item, they should all render.
Also, you'll have to be sure to clear the selectedGroup whenever there are no selected items.
If this does not work, it may be that your state changes are not triggering a re-render. In which case it can be as simple as adding a reference to your state object in your component's return method like this:
{blockState.filterGroupSelected && " " }
It's a hack but it works, and keeps you from adding calls to useEffect.

Looping to clear all numbers colors in react

I have this working function to change all children button color on click.
Now im doing a "ClearGame" button, to change all button background color to its original state '#ADC0C4'. How can i do that with key or id from the children?
const newBet: React.FC = () => {
const clearGame = () => {
let spliceRangeJSON = gamesJson[whichLoteriaIsVar].range;
totalNumbers.splice(0, spliceRangeJSON);
for (let i = 0; i <= spliceRangeJSON; i++) {
//Looping to change the backgroundColor using Id or Key
}
};
const NumbersParent = (props: any) => {
const [numbersColor, setNumbersColor] = useState('#ADC0C4');
const changeButtonColor = () => {
if (numbersColor === '#ADC0C4') {
setNumbersColor(gamesJson[whichLoteriaIsVar].color);
totalNumbers.push(props.id);
} else {
setNumbersColor('#ADC0C4');
let searchTotalNumbers = totalNumbers.indexOf(props.id);
totalNumbers.splice(searchTotalNumbers, 1);
}
};
return (
<Numbers style={{ backgroundColor: numbersColor }} onClick={changeButtonColor}>
{props.children}
</Numbers>
);
};
return (
<NumbersContainer>
{numbersList.map((num) => (
<NumbersParent key={num} id={num}>
{formatNumber(num)}
</NumbersParent>
))}
</NumbersContainer>
<ClearGame onClick{clearGame}>Clear Game</ClearGame >
);
};
Since you want to modify the state of children from the parent component,
Create a State Object in the parent.
Pass it to the children as prop
You can change it on clear.
Or try something like recoil.
Move the useState and changeButtonColor method to the parent component and make some changes:
const [numbersColor, setNumbersColor] = useState({});
const changeButtonColor = (color) => {
if (!numbersColor[color] || numbersColor[color] === '#ADC0C4') {
setNumbersColor({...numbersColors,
[color]: gamesJson[whichLoteriaIsVar].color
});
totalNumbers.push(props.id);
} else {
setNumbersColor({...numbersColors,
[color]: '#ADC0C4'
});
let searchTotalNumbers = totalNumbers.indexOf(props.id);
totalNumbers.splice(searchTotalNumbers, 1);
}
};
Change the map to that:
<NumbersContainer>
{numbersList.map((num, index) => (
<NumbersParent key={num} id={num} index={index} changeButtonColor={(color) => {changeButtonColor(color)}}>
{formatNumber(num)}
</NumbersParent>
))}
</NumbersContainer>
And on the return of NumbersParent component changes to be like that:
<Numbers style={{ backgroundColor: numbersColor[`color${props.index}`] }} onClick={props.changeButtonColor(`color${props.index}`)}>
{props.children}
</Numbers>
Finally, change clearGame function like this:
const clearGame = () => {
let spliceRangeJSON = gamesJson[whichLoteriaIsVar].range;
for (let i = 0; i <= spliceRangeJSON; i++) {
changeButtonColor(`color${i}`)
}
totalNumbers.splice(0, spliceRangeJSON);
};

How to keep every two identical names red if both were clicked consecutively?

I have list items represent names, when clicking any name it turns red then take one second to return black again, but clicking two identical names consecutively make them keep red color, not turning black again
you can imagine it as a memory game, but i tried to make a simple example here of what i am trying to achieve in the original project
This is my code and my wrong trial:
const App = () => {
const { useState } = React;
const items = [
{
name: 'mark',
id: 1,
red: false
},
{
name: 'peter',
id: 2,
red: false
},
{
name: 'john',
id: 3,
red: false
},
{
name: 'mark',
id: 4,
red: false,
},
{
name: 'peter',
id: 5,
red: false
},
{
name: 'john',
id: 6,
red: false
}
];
const [names, setNames] = useState(items);
const [firstName, setFirstName] = useState(null);
const [secondName, setSecondName] = useState(null)
const handleItemClick = (item) => {
setNames(prev => prev.map(i => i.id === item.id ? { ...i, red: true } : i));
//the problem is here
setTimeout(() => {
setNames(prev => prev.map(n => {
if (secondName && (secondName.name === firstName.name) && n.name === firstName.name) {
return { ...n,red: true }
}
return { ...n, red: false };
}))
}, 1000)
if (!firstName) setFirstName(item);
else if (firstName && !secondName) setSecondName(item)
else if (firstName && secondName) {
setFirstName(item);
setSecondName(null)
}
}
return (
<div class="app">
<ul class="items">
{
names.map(i => {
return (
<Item
item={i}
handleItemClick={handleItemClick}
/>
)
})
}
</ul>
</div>
)
}
const Item = ({ item, ...props }) => {
const { id, name, red } = item;
const { handleItemClick } = props;
return (
<li
className={`${red ? 'red' : ''}`}
onClick={() => handleItemClick(item)}
>
{name}
</li>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
But this code doesn't work correctly, when clicking two identical names consecutively they don't keep red color and turning black again
To me it seems the issue is overloading the event handler and violating the Single Responsibility Principle.
The handler should be responsible for handling the click event and nothing else. In this case, when the element is clicked you want to add the id to the state of selected/picked names, and toggle the red state value of item with matching id. Factor the timeout effect into (strangely enough) an useEffect hook, with the picks as dependencies. This inverts the logic of the timeout to clearing/resetting the state versus setting what is "red" or not. You can/should also move any logic of determining matches into this same effect (since it already has the dependencies anyway).
useEffect(() => {
... logic to determine matches
const timer = setTimeout(() => {
// time expired, reset only if two names selected
if (firstName && secondName) {
setFirstName(null);
setSecondName(null);
setNames(names => names.map(name => ({ ...name, red: false })));
}
}, 1000);
// clean up old timeout when state updates, i.e. new selected
return () => clearTimeout(timer);
}, [firstName, secondName]);
This will allow you to simplify your name setting logic to
if (!firstName) {
setFirstName(item);
} else {
setSecondName(item);
}
Note: I believe you need another data structure to hold/track/store existing matches made by the user.
How this works:
Starting from clean state, no names are chosen
When first name is picked, firstName is null and updated, red state updated
Timeout is set (but won't clear state yet)
When second name is picked, firstName is defined, so secondName is updated, red state updated
If match, add match to state (to keep red)
Timeout expire and reset state (go back to step 1)
The following is how I'd try to simplify state a bit more, using an array of selected ids that only update if the selected id isn't already chosen and 2 picks haven't been chosen yet.
const App = () => {
const [names, setNames] = useState(items);
const [picks, setPicks] = useState([]);
const [matched, setMatched] = useState({});
/**
* On click event, add id to `picks` array, allow only two picks
*/
const onClickHandler = id => () =>
picks.length !== 2 &&
!picks.includes(id) &&
setPicks(picks => [...picks, id]);
/**
* Effect to toggle red state if id is included in current picks
*/
useEffect(() => {
setNames(names =>
names.map(name => ({
...name,
red: picks.includes(name.id)
}))
);
}, [picks]);
/**
* Effect checks for name match, if a match is found it is added to the
* `matched` array.
*/
useEffect(() => {
// matches example: { mark: 1, peter: 0, john: 0 }
const matches = names.reduce((matches, { name, red }) => {
if (!matches[name]) matches[name] = 0;
red && matches[name]++;
return matches;
}, {});
const match = Object.entries(matches).find(([_, count]) => count === 2);
if (match) {
const [matchedName] = match;
setMatched(matched => ({ ...matched, [matchedName]: matchedName }));
}
const timer = setTimeout(() => {
if (picks.length === 2) {
setPicks([]);
setNames(names => names.map(name => ({ ...name, red: false })));
}
}, 1000);
return () => clearTimeout(timer);
}, [names, picks]);
return (
<div className="App">
<ul>
{names.map(item => (
<Item
key={item.id}
item={item}
matches={matched}
onClick={onClickHandler(item.id)}
/>
))}
</ul>
</div>
);
};
const Item = ({ item, matches, ...props }) => {
const { name, red } = item;
return (
<li
className={classnames({
red: red || matches[name], // for red text color
matched: matches[name] // any other style to make matches stand out
})}
{...props}
>
{name}
</li>
);
};

how to add multiple objects in reactjs?

I want to add new Objects when user click on checkbox. For example , When user click on group , it will store data {permission:{group:["1","2"]}}. If I click on topgroup , it will store new objects with previous one
{permission:{group:["1","2"]},{topGroup:["1","2"]}}.
1st : The problem is that I can not merge new object with previous one . I saw only one objects each time when I click on the group or topgroup.
onChange = value => checked => {
this.setState({ checked }, () => {
this.setState(prevState => {
Object.assign(prevState.permission, { [value]: this.state.checked });
});
});
};
<CheckboxGroup
options={options}
value={checked}
onChange={this.onChange(this.props.label)}
/>
Here is my codesanbox:https://codesandbox.io/s/stackoverflow-a-60764570-3982562-v1-0qh67
It is a lot of code because I've added set and get to set and get state. Now you can store the path to the state in permissionsKey and topGroupKey. You can put get and set in a separate lib.js.
In this example Row is pretty much stateless and App holds it's state, this way App can do something with the values once the user is finished checking/unchecking what it needs.
const Checkbox = antd.Checkbox;
const CheckboxGroup = Checkbox.Group;
class Row extends React.Component {
isAllChecked = () => {
const { options, checked } = this.props;
return checked.length === options.length;
};
isIndeterminate = () => {
const { options, checked } = this.props;
return (
checked.length > 0 && checked.length < options.length
);
};
render() {
const {
options,
checked,
onChange,
onToggleAll,
stateKey,
label,
} = this.props; //all data and behaviour is passed by App
return (
<div>
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.isIndeterminate()}
onChange={e =>
onToggleAll(e.target.checked, stateKey)
}
checked={this.isAllChecked()}
>
Check all {label}
</Checkbox>
<CheckboxGroup
options={options}
value={checked}
onChange={val => {
onChange(stateKey, val);
}}
/>
</div>
</div>
);
}
}
//helper from https://gist.github.com/amsterdamharu/659bb39912096e74ba1c8c676948d5d9
const REMOVE = () => REMOVE;
const get = (object, path, defaultValue) => {
const recur = (current, path) => {
if (current === undefined) {
return defaultValue;
}
if (path.length === 0) {
return current;
}
return recur(current[path[0]], path.slice(1));
};
return recur(object, path);
};
const set = (object, path, callback) => {
const setKey = (current, key, value) => {
if (Array.isArray(current)) {
return value === REMOVE
? current.filter((_, i) => key !== i)
: current.map((c, i) => (i === key ? value : c));
}
return value === REMOVE
? Object.entries(current).reduce((result, [k, v]) => {
if (k !== key) {
result[k] = v;
}
return result;
}, {})
: { ...current, [key]: value };
};
const recur = (current, path) => {
if (path.length === 1) {
return setKey(
current,
path[0],
callback(current[path[0]])
);
}
return setKey(
current,
path[0],
recur(current[path[0]], path.slice(1))
);
};
return recur(object, path, callback);
};
class App extends React.Component {
state = {
permission: { group: [] },
topGroup: [],
some: { other: [{ nested: { state: [] } }] },
};
permissionsKey = ['permission', 'group']; //where to find permissions in state
topGroupKey = ['topGroup']; //where to find top group in state
someKey = ['some', 'other', 0, 'nested', 'state']; //where other group is in state
onChange = (key, value) => {
//use set helper to set state
this.setState(set(this.state, key, arr => value));
};
isIndeterminate = () =>
!this.isEverythingChecked() &&
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
result || get(this.state, key).length,
false
);
toggleEveryting = e => {
const checked = e.target.checked;
this.setState(
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
set(result, key, () =>
checked
? this.plainOptions.map(({ value }) => value)
: []
),
this.state
)
);
};
onToggleAll = (checked, key) => {
this.setState(
//use set helper to set state
set(this.state, key, () =>
checked
? this.plainOptions.map(({ value }) => value)
: []
)
);
};
isEverythingChecked = () =>
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
result &&
get(this.state, key).length ===
this.plainOptions.length,
true
);
plainOptions = [
{ value: 1, name: 'Apple' },
{ value: 2, name: 'Pear' },
{ value: 3, name: 'Orange' },
];
render() {
return (
<React.Fragment>
<h1>App state</h1>
{JSON.stringify(this.state)}
<div>
<Checkbox
indeterminate={this.isIndeterminate()}
onChange={this.toggleEveryting}
checked={this.isEverythingChecked()}
>
Toggle everything
</Checkbox>
</div>
{[
{ label: 'group', stateKey: this.permissionsKey },
{ label: 'top', stateKey: this.topGroupKey },
{ label: 'other', stateKey: this.someKey },
].map(({ label, stateKey }) => (
<Row
key={label}
options={this.plainOptions}
// use getter to get state selected value
// for this particular group
checked={get(this.state, stateKey)}
label={label}
onChange={this.onChange} //change behaviour from App
onToggleAll={this.onToggleAll} //toggle all from App
//state key to indicate what state needs to change
// used in setState in App and passed to set helper
stateKey={stateKey}
/>
))}
</React.Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<link href="https://cdnjs.cloudflare.com/ajax/libs/antd/4.0.3/antd.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/antd/4.0.3/antd.js"></script>
<div id="root"></div>
I rewrite all the handlers.
The bug in your code is located on the usage of antd Checkbox.Group component with map as a child component, perhaps we need some key to distinguish each of the Row. Simply put them in one component works without that strange state update.
As the demand during communication, the total button is also added.
And, we don't need many states, keep the single-source data is always the best practice.
import React from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { Checkbox } from "antd";
const group = ["group", "top"];
const groupItems = ["Apple", "Pear", "Orange"];
const CheckboxGroup = Checkbox.Group;
class App extends React.Component {
constructor() {
super();
this.state = {
permission: {}
};
}
UNSAFE_componentWillMount() {
this.setDefault(false);
}
setDefault = fill => {
const temp = {};
group.forEach(x => (temp[x] = fill ? groupItems : []));
this.setState({ permission: temp });
};
checkLength = () => {
const { permission } = this.state;
let sum = 0;
Object.keys(permission).forEach(x => (sum += permission[x].length));
return sum;
};
/**
* For total
*/
isTotalIndeterminate = () => {
const len = this.checkLength();
return len > 0 && len < groupItems.length * group.length;
};
onCheckTotalChange = () => e => {
this.setDefault(e.target.checked);
};
isTotalChecked = () => {
return this.checkLength() === groupItems.length * group.length;
};
/**
* For each group
*/
isIndeterminate = label => {
const { permission } = this.state;
return (
permission[label].length > 0 &&
permission[label].length < groupItems.length
);
};
onCheckAllChange = label => e => {
const { permission } = this.state;
const list = e.target.checked ? groupItems : [];
this.setState({ permission: { ...permission, [label]: list } });
};
isAllChecked = label => {
const { permission } = this.state;
return !groupItems.some(x => !permission[label].includes(x));
};
/**
* For each item
*/
isChecked = label => {
const { permission } = this.state;
return permission[label];
};
onChange = label => e => {
const { permission } = this.state;
this.setState({ permission: { ...permission, [label]: e } });
};
render() {
const { permission } = this.state;
console.log(permission);
return (
<React.Fragment>
<Checkbox
indeterminate={this.isTotalIndeterminate()}
onChange={this.onCheckTotalChange()}
checked={this.isTotalChecked()}
>
Check all
</Checkbox>
{group.map(label => (
<div key={label}>
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.isIndeterminate(label)}
onChange={this.onCheckAllChange(label)}
checked={this.isAllChecked(label)}
>
Check all
</Checkbox>
<CheckboxGroup
options={groupItems}
value={this.isChecked(label)}
onChange={this.onChange(label)}
/>
</div>
</div>
))}
</React.Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById("container"));
Try it online:
Please try this,
onChange = value => checked => {
this.setState({ checked }, () => {
this.setState(prevState => {
permission : { ...prevSatate.permission , { [value]: this.state.checked }}
});
});
};
by using spread operator you can stop mutating the object. same way you can also use object.assign like this.
this.setState(prevState => {
permission : Object.assign({} , prevState.permission, { [value]: this.state.checked });
});
And also i would suggest not to call setState in a callback. If you want to access the current state you can simply use the current checked value which you are getting in the function itself.
so your function becomes ,
onChange = value => checked => {
this.setState({ checked });
this.setState(prevState => {return { permission : { ...prevSatate.permission, { [value]: checked }}
}});
};
Try the following
//Inside constructor do the following
this.state = {checkState:[]}
this.setChecked = this.setChecked.bind(this);
//this.setChecked2 = this.setChecked2.bind(this);
//Outside constructor but before render()
setChecked(e){
this.setState({
checkState : this.state.checkState.concat([{checked: e.target.id + '=>' + e.target.value}])
//Id is the id property for a specific(target) field
});
}
//Finally attack the method above.i.e. this.setChecked to a form input.
Hope it will address your issues

Categories

Resources