I have been practicing JavaScript and ReactJs and I have been stuck on a problem for a while. Basically, I am trying to rewrite my HTML, CSS, Javascript project using ReactJs.
Here is my problem: (in regards to the React code). Say I click on the first answer choice for Question 1, the class name changes hence, styling changes(background becomes black) AND isClicked becomes true (both are states inside the EachIndividualAnswer class). If I then click on the second answer choice, I want the style for the first answer choice (and every other answer choice for that question) to be null, and isClicked to be false and ONLY the second answer will have isClicked === true and className="clicked".
Hope this makes sense. Sorry for sending so many files, Didn't know any other way.
Thanks
MY HTML, CSS AND JAVASCRIPT CODE. (the code I am trying to re-write with ReactJs)
<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>
var numberOfQuestions = 5;
var choicesPerQuestion = 5;
var questionNumber = document.getElementsByClassName("questionNumber");
var question = document.getElementsByClassName("question");
var answers = document.getElementsByClassName("answers");
var answer_A = document.getElementsByClassName("answer_A");
var answer_B = document.getElementsByClassName("answer_B");
var answer_C = document.getElementsByClassName("answer_C");
var answer_D = document.getElementsByClassName("answer_D");
var answer_E = document.getElementsByClassName("answer_E");
var submit = document.getElementById("submit");
// Answer key
var answerKey = [21, 3, "Nani", "Kevin Durant", "Russ"];
var userAnswerArray = new Array(5);
// Put every single possible clickable answer in 5x5 array
// clicking an answer changes its background and color
var individual_answers = new Array(numberOfQuestions);
for(let i=0; i<numberOfQuestions; i++) {
individual_answers[i] = new Array(choicesPerQuestion);
}
// Adding Event listeners to each answer choice
for (let i = 0; i < answers.length; i++) {
specificAnswers = answers[i].getElementsByTagName("li"); // answers to each questions e.g. answers to qu.1, then qu.2
for (let j = 0; j < specificAnswers.length; j++) {
individual_answers[i][j] = specificAnswers[j]; // individual answers to each qu.
var spanX = individual_answers[i][j].getElementsByTagName("span"); // did not use this
individual_answers[i][j].addEventListener("click", click(i , j));
}
}
function click(i, j) {
return function() {
console.log(individual_answers[i][j].innerText);
if(individual_answers[i][j].style.background != "black") { // if it's not black, set all to white, then put specific one to black
for(let x=0; x<choicesPerQuestion; x++) {
individual_answers[i][x].style.cssText = "background: white";
individual_answers[i][x].getElementsByTagName("span")[0].style.color = "black";
}
individual_answers[i][j].style.cssText = "background: black";
individual_answers[i][j].style.color = "green";
individual_answers[i][j].getElementsByTagName("span")[0].style.color = "white";
userAnswerArray[i] = individual_answers[i][j].innerText;
// i = question number, j = specific answer to question number i
// So on each click, if answer originally doesn't have a black background, add it to userArray
}
else { // If background is black, on click you have to remove that from individual array
individual_answers[i][j].style.cssText = "background: white";
individual_answers[i][j].getElementsByTagName("span")[0].style.color = "black";
userAnswerArray.splice(i, 1);
}
}
}
// Adding event listener to submit button
submit.addEventListener("click", score);
/* Easiest thing to do would be to make an "Answer class" for each answer. (using prototypes) with field selected.
Then count the number of answers with fields selected and compare with answer key or smthn. Try this as an exercise for later, maybe ReactJs */
/* For now I will create an array for the answers that will change as the user clicks and use the actual words to see if they match */
function score() {
/* Add a check later to see if he has answered every question or at least 60% */
var counter = 0;
for(let x=0; x<numberOfQuestions; x++) {
if(answerKey[x] == userAnswerArray[x]) {
counter++;
}
}
console.log("User has submitted the quiz and scored " + counter);
if(counter < 3) {
alert("Try again, you failed");
}
else {
alert("Are you a lizard?")
}
/* Show on a message and ask to retake*/
}
ul {
list-style-type: square;
}
ul > li {
color: blue;
font-size: 30px;
}
ul > li > span {
color: black;
font-size: 20px;
}
#submit {
font-size: 30px;
}
<!--I will try to create a multiple choice exam. The user can NOT submit until he has answered 60% of the questions. Once he submits
I will show him his score. Give him the option to see which questions he failed, as well as the right answer. -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Multiple Choice Exam</title>
<link rel="stylesheet" href="mcq.css">
</head>
<body>
<h1>NASA Final Entry Exam</h1>
<h2>Only the most genius of individuals will pass</h2>
<br>
<hr>
<br>
<p class="question"><span class="questionOne">1</span>. What is 9+10</p>
<ul class="answers" id="answers1">
<li class="answer_A"><span>1</span></li>
<li class="answer_B"><span>19</span></li>
<li class="answer_C"><span>21</span></li>
<li class="answer_D"><span>90</span></li>
<li class="answer_E"><span>-1<span></li>
</ul>
<p class="question"><span class="questiontwo">2</span>. How many goals did Ronaldo score against Spain in the World Cup 2018</p>
<ul class="answers">
<li class="answer_A"><span>1</span></li>
<li class="answer_B"><span>3</span></li>
<li class="answer_C"><span>5</span></li>
<li class="answer_D"><span>0</span></li>
<li class="answer_E"><span>-1<span></li>
</ul>
<p class="question"><span class="questionThree">3</span>. Who Stole Ronaldo's (CR7) greates ever goal?</p>
<ul class="answers">
<li class="answer_A"><span>Pepe</span></li>
<li class="answer_B"><span>Messi</span></li>
<li class="answer_C"><span>Casillas</span></li>
<li class="answer_D"><span>Benzema</span></li>
<li class="answer_E"><span>Nani<span></li>
</ul>
<p class="question"><span class="questionFour">4</span>. Which one of these players ruined the NBA</p>
<ul class="answers">
<li class="answer_A"><span>Allen Iverson</span></li>
<li class="answer_B"><span>Kevin Durant</span></li>
<li class="answer_C"><span>Steph Curry</span></li>
<li class="answer_D"><span>Lebron James</span></li>
<li class="answer_E"><span>Russel Westbrook<span></li>
</ul>
<p class="question"><span class="questionFive">5</span>. Who is currently number 1 in the internet L ranking?</p>
<ul class="answers">
<li class="answer_A"><span>Drake</span></li>
<li class="answer_B"><span>Pusha T</span></li>
<li class="answer_C"><span>Russel WestBrook</span></li>
<li class="answer_D"><span>Lil Xan</span></li>
<li class="answer_E"><span>Russ<span></li>
</ul>
<button id="submit">Submit</button>
<script src="mcq.js"></script>
</body>
</html>
HERE IS MY REACJS PROJECT SO FAR. Wasn't sure on how to properly upload these files:
[App.js]
import React, { Component } from 'react';
import './App.css';
import Title from './Title/Title';
import Question from './Question/Question';
import Aux from './hoc/Aux';
class App extends Component {
state = {
counter: 0,
questionArray: [
"What is 9+10",
"How many goals did Ronaldo score against Spain in the World Cup 2018",
"Who Stole Ronaldo's (CR7) greates ever goal?",
"Which one of these players ruined the NBA",
"Who is currently number 1 in the internet L rankings?"
],
answerChoicesArray: [
["1", "19", "21", "90", "-1"],
["1", "3", "5", "0", "-1"],
["Pepe", "Messi", "Casillas", "Benzema", "Nani"],
["Allen Iverson", "Kevin Durant", "Steph Curry", "Lebron James", "Russel Westbrook"],
["Drake", "Pusha T", "Russel Westbrook", "Lil Xan", "Russ"]
]
}
render() {
return (
<div className="App">
<div className="container">
<Aux>
<Title />
<h2>Only the most genius of individuals will pass</h2>
<hr/>
<Question
questionArray={this.state.questionArray}
answerChoicesArray={this.state.answerChoicesArray} />
<button
onClick={() => alert("We don't support this yet")}
type="submit">SUBMIT</button>
</Aux>
</div>
</div>
);
}
}
export default App;
.
.
[Question.js]
import React from 'react';
import AnswerChoices from '../AnswersChoices/AnswerChoices';
const Question = (props) => // why doesn't it work if I put a curly brace here
props.questionArray.map((question, index) => {
return(
<div>
<p>{index + 1}. {question}</p>
<AnswerChoices
index={index} // try just index answersArray is the array of ALL answers
answerChoicesArray={props.answerChoicesArray} />
</div>
);
})
export default Question;
.
.
[AnswerChoices.js]
import React from 'react';
import SpecificAnswerChoice from './SpecificAnswerChoice/SpecificAnswerChoice'
const AnswerChoices = (props) => {
console.log(props.answerChoicesArray[props.index]);
return (
// 5 answers array for each question
<div>
<ul>
<SpecificAnswerChoice
answers={props.answerChoicesArray[props.index]}/>
</ul>
</div>
)
}
export default AnswerChoices;
.
.
[SpecificAnswerChoice.js]
import React, { Component } from 'react';
import EachIndividualAnswer from './EachIndividualAnswer/EachIndividualAnswer'
class SpecificAnswerChoice extends Component {
// If I click once, set all to white and specific to black
state = {
resetClicksState: true // can start w/ false then change to always true inside resetClicks function
}
resetClicks = () => {
console.log("TEST");
}
render() {
// const style = {
// backgroundColor: 'white'
// };
return(
this.props.answers.map(individualAnswer => {
return (
<EachIndividualAnswer
className={this.state.class}
individualAnswer={individualAnswer}
resetClicks={this.resetClicks}
// onClick={this.clickHandler}
/>
);
})
)
}
}
export default SpecificAnswerChoice;
import React, { Component } from 'react';
.
.
[EachIndividualAnswer.js]
class EachIndividualAnswer extends Component {
state = {
isClicked: false,
class: ""
}
// clickHandler = (style) => {
// if(style.backgroundColor === 'white') {
// style.backgroundColor = 'black';
// style.color = 'white';
// }
// }
onClickHandler = () => {
console.log(this.state.isClicked);
console.log("djhfdf");
if(this.state.isClicked) {
var tempClass=""
this.setState({
isClicked: false,
class: tempClass
});
} else {
tempClass="clicked"
this.setState({
isClicked: true,
class:tempClass
})
}
this.props.resetClicks();
}
// testingOnClick = () => {
// console.log("If this works then I have 2+ functions on OnClick");
// }
// if props.resetClicks is true, which it always is, className='', isClicked=false for EVERY
// EachIndividualAnswer. Then I do my logc that I already had
render() {
return (<li
className={this.state.class}
onClick={this.onClickHandler}>
<span>
{this.props.individualAnswer}
</span>
</li>);
}
}
export default EachIndividualAnswer;
.
.
[Aux.js]
const aux = (props) => props.children;
export default aux;
Here is one possible answer if I understood you right. I'm totally mimicking the situation so this is not a complete solution for you.
class App extends React.Component {
state = {
answers: [ "1", "19", "21", "90", "-1" ],
selected: {},
}
handleClick = e => {
const {answer} = e.target.dataset;
this.setState({selected:{
[answer]: !!answer,
}})
};
render() {
const {answers} = this.state;
console.log(this.state.selected);
return (
<div>
<ul>
{
answers.map( answer =>
<li
data-answer={answer}
className={
this.state.selected[answer] ? 'colored' : ''
}
onClick={this.handleClick}
>{answer}
</li>
)
}
</ul>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
.colored {
color: red;
}
<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>
Here, we are using a selected state to hold the selected situation and according to this situation we are adding our class or set it to null. Our handleClick function does this selection change. There is a console.log in the render method so you can see what is going on here.
Also, I used dataset here to get the value since I don't like binding functions in JSX. With .bind it can be like that. Only the relevant parts:
handleClick2 = answer =>
this.setState({
selected: {
[answer]: !!answer,
}
})
and
onClick={this.handleClick2.bind(this, answer)}
One other possible solution to here, instead of doing this logic in your EachIndividualAnswer component you can do it in your SpecificAnswerChoice component. I mean holding the state and having handleClick handler. So, you can pass this handler to your EachIndividualAnswer with the answer than with your callback you can set the state in the EachIndividualAnswer. So, there will be no need to use datasets or .bind.
Lastly, as other says in the comments you should share a minimal code where you have problems. So, people can look your code easily and do their best.
If you think holding long answers as object properties is silly, here is another answer using array indexes of answers:
class App extends React.Component {
state = {
answers: [ "1", "19", "21", "90", "-1" ],
selected: {},
}
handleClick = e => {
const {index} = e.target.dataset;
this.setState({selected:{
[index]: !!index,
}})
};
render() {
const {answers} = this.state;
console.log(this.state.selected);
return (
<div>
<ul>
{
answers.map( (answer, index) =>
<li
data-index={index}
className={
this.state.selected[index] ? 'colored' : ''
}
onClick={this.handleClick}
>{answer}
</li>
)
}
</ul>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
.colored {
color: red;
}
<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>
Edit after questions in comments
const {answers} = this.state
This is Javascript's destructuring assignment syntax. We are just picking a property from the this.state object. Shorthand version of this code:
const answers = this.state.answers;
At first this could be seem not so useful but you can pick as many property as you want in an object. Think about a crowded object and you just need just three of them:
const { one, two, three } = object;
This is the shorthand of:
const one = object.one;
const two = object.two;
const three = object.three;
Very useful. For further information just read this answer and of course the official documentation. You can see destructring all around React world since some people tend to use where it is suitable. It may not be always so nice when it is overused. It makes harder to read the code sometimes. But when you use it in the suitable places, it saves time and even makes readability better.
We are keeping answers here in our state. In your original code you are getting them as props. I will provide another answer after doing the explanation based on props. selected is the real deal here, state that we keep our selected elements.
this.setState({selected:{ [answer]: !!answer }})
Yes, this is a little bit awkward. We are using computed property for objects. So, we can use variables in an object's property. [answer], this is what we use. So, in the selected state we are setting a property which name is our answer variable. Now, we use the right hand side to use our value to a boolean and we are setting it always to true in the first state change.
answer variable is a string here. In Javascript if you use logical not, ! , operator on a string it evaluates to false. So we are using it twice to get true. For example, when we click on "19" this would be like this:
selected: {
"19": !!"19"
}
You can try !!"19" in Javascript console, you will get true. Instead of using real values, we are just using variables here: [answer]: !!answer
Now, I will change this syntax a little bit in my last code example. If you look setState's documentation you will see it is an asynchronous operation. So, React team discourages us to use it directly like this, especially if we use previous state of any piece in our state. Actually we are not doing it like that in our example but it is better using a callback for this.setState. Please go and read official documentation if this explanation is not enough for you. Here how we use it this time:
handleClick = answer => {
this.setState(prevState =>
({
selected: {
[answer]: !prevState.selected.answer,
}
})
)
};
As you can see setState here takes a callback and uses prevState (or what name you give it) to react the previous state. Now, since there is not any selected.answer in our previous state, it is undefined in the first place. So, we can use !prevState.selected.answer to make the value true instead of using two logical not operand here. Remember, in the previous example we have a string here not an undefined value. This is why we use two logical not operand there.
Now, here is the last code that suits your situation. You are getting answers as prop and then render another component to show those. I use three components like you then render the individual answers.
const answers = ["1", "19", "21", "90", "-1"]
const AnswerChoices = () => (
<SpecificAnswerChoice answers={answers} />
)
class SpecificAnswerChoice extends React.Component {
state = {
selected: {},
}
handleClick = answer =>
this.setState(prevState =>
({
selected: {
[answer]: !prevState.selected.answer,
}
})
);
render() {
const { answers } = this.props;
return (
<div>
{
answers.map( individualAnswer => (
<EachIndividualAnswer
individualAnswer={individualAnswer}
onClick={this.handleClick}
selected={this.state.selected}
key={individualAnswer}
/>
) )
}
</div>
)
}
}
// Again, destructring. Instead of (props) we use ({....})
// and pick our individual props here.
const EachIndividualAnswer = ({selected,individualAnswer, onClick}) => {
const handleClick = () => onClick(individualAnswer)
return (
<div>
<ul>
{
<li
onClick={handleClick}
className={
selected[individualAnswer] ? 'colored' : ''
}
>{individualAnswer}
</li>
}
</ul>
</div>
)
}
const rootElement = document.getElementById("root");
ReactDOM.render(<AnswerChoices />, rootElement);
.colored {
color: red;
}
<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>
I declared EachIndividualAnswer component as a functional one since it does not need to be a class. Also, I pass a handleClick prop for click event. With this handler, child sends the answer and parent component gets back it and updates its state. One prop EachIndividualAnswer gets is selected state. So it decides whether add a class or not. So, our selected state resides in SpecificAnswerChoice component and child gets it as a prop. Lastly, this component gets answers from its parent as you can see.
Related
The following is the code for my component
import React from 'react'
import Wrapper from '../../assets/css/LandingPageSmallerSectionCss/faqSectioncss'
// import FaqComponent from '../../components/faqComponent'
import faqs from '../../utils/faqs'
const FaqSection = () => {
const ref = React.createRef();
const toggleHandler = (e) => {
e.preventDefault()
ref.current.classList.toggle('.show-more')
}
return (
<Wrapper>
<div className="faq-container">
<h1 className="faqs-header" style={{ color: "rgb(40, 102, 129)" }}>FAQs</h1>
<div className="list-container">
<ul>
{faqs.map((faq) => {
const { id, question, answer } = faq
return (
<li key={id}>
<p>{question}</p>
<button className="toggle-more-btn" onClick={toggleHandler}></button>
<p className="faq-text" ref={ref}>{answer}</p>
</li>
)
})}
</ul>
</div>
</div>
</Wrapper>
)
}
export default FaqSection
I can implement the code for toggling in pure javascript and plain html but i'm having trouble translating it to react since react does not have queryselectorAll hence i'm having trouble,the following is the code for the pure javascript
let listContent= document.querySelectorAll('.faq-text')
let buttonContent = document.querySelectorAll('.toggle-more-btn')
const listArray=Array.from(listContent)
const buttonArray=Array.from(buttonContent)
//display both array list
// console.log(listArray)
// console.log(buttonArray)
//check if individual examples buttons are coherent with each other
// if (buttonArray[0] && listArray[0]) {
// buttonArray[0].addEventListener('click',(e)=>{
// listArray[0].classList.toggle('poopoo')
// })
// }
// if (buttonArray[1] && listArray[1]) {
// buttonArray[1].addEventListener('click',(e)=>{
// listArray[1].classList.toggle('poopoo')
// })
// }
//loop through all buttons in the list and if both the index of the button and list match
// for (let i = 0; i < buttonArray.length; i++) {
// if (buttonArray[i]&&listArray[i]) {
// buttonArray[i].addEventListener('click',e=>{
// listArray[i].classList.toggle('poopoo')
// })}
// }
// create funcion that works with individual array indexes
//experimental
const buttonPress=(button,list)=>{
for (let i = 0; i < button.length; i++) {
if (button[i]&&list[i]) {
button[i].addEventListener('click',e=>{
list[i].classList.toggle('show-more')
button[i].classList.toggle('rotate-btn')
})
}
}
}
buttonPress(buttonArray,listArray)
I've spent hours on this.
If anyone could answer me i'd be greatfull,kindly?
It is a couple things so far:
Handler function is not named the same as what is called.
The DOT on your css class .show-more! It took awhile (and a console.log to find this).
I don't think you want a ref in the first place.
Here is what I might do using some hooks. I'm faking first load with the useEffect, with empty array arg.
Then, I'm going to set the changed faq entry in a clone of the list "faqs", and then spread my temp array into the setFaqs. This is using the useState hook, which generally you give a variable name and then a setter function name (and for these built-ins React takes care of actually setting up in the background - including the default I have an empty array).
import { useEffect, useState } from "react";
import "./styles.css";
const mockServerReturn = [
{ id: 0, question: "Foo?", answer: "Bar!" },
{ id: 1, question: "Baz?", answer: "Bar!" },
{ id: 2, question: "Bar?", answer: "Bar!" }
];
const FaqSection = () => {
const [faqs, setFaqs] = useState([]);
useEffect(() => setFaqs(mockServerReturn), []);
const toggleHandler = (e) => {
e.preventDefault();
const temp = [...faqs];
temp[e.target.id].chosen = !temp[e.target.id].chosen;
setFaqs([...temp]);
};
return (
<>
<div className="faq-container">
<h1 className="faqs-header">FAQs</h1>
<div className="list-container">
<ul>
{faqs.map((faq) => {
const { id, question, answer, chosen } = faq;
return (
<li key={id}>
<p>{question}</p>
<button
className="toggle-more-btn"
onClick={toggleHandler}
id={id}
>
I choose {id}
</button>
<p className={`faq-text${chosen ? " show-more" : ""}`}>
{id}:{answer}:{chosen?.toString()}
</p>
</li>
);
})}
</ul>
</div>
</div>
</>
);
};
export default function App() {
return (
<div className="App">
<FaqSection />
</div>
);
}
minimum reproducible example: https://codesandbox.io/s/react-hover-example-tu1eu?file=/index.js
I currently have a new element being rendered when either of 2 other elements are hovered over. But i would like to render different things based upon which element is hovered.
In the example below and in the codepen, there are 2 hoverable divs that are rendered; when they are hovered over, it changes the state and another div is rendered. I would like for the HoverMe2 div to render text "hello2". Currently, whether i hover hoverme1 or 2, they both just render the text "hello".
import React, { Component } from "react";
import { render } from "react-dom";
class HoverExample extends Component {
constructor(props) {
super(props);
this.handleMouseHover = this.handleMouseHover.bind(this);
this.state = {
isHovering: false
};
}
handleMouseHover() {
this.setState(this.toggleHoverState);
}
toggleHoverState(state) {
return {
isHovering: !state.isHovering
};
}
render() {
return (
<div>
<div
onMouseEnter={this.handleMouseHover}
onMouseLeave={this.handleMouseHover}
>
Hover Me
</div>
<div
onMouseEnter={this.handleMouseHover}
onMouseLeave={this.handleMouseHover}
>
Hover Me2
</div>
{this.state.isHovering && <div>hello</div>}
</div>
);
}
}
render(<HoverExample />, document.getElementById("root"));
You need to keep the state of item which you have hovered that's for sure
const { Component, useState, useEffect } = React;
class HoverExample extends Component {
constructor(props) {
super(props);
this.handleMouseHover = this.handleMouseHover.bind(this);
this.state = {
isHovering: false,
values: ['hello', 'hello2'],
value: 'hello'
};
}
handleMouseHover({target: {dataset: {id}}}) {
this.setState(state => {
return {
...state,
isHovering: !state.isHovering,
value: state.values[id]
};
});
}
render() {
return (
<div>
<div
data-id="0"
onMouseEnter={this.handleMouseHover}
onMouseLeave={this.handleMouseHover}
>
Hover Me
</div>
<div
data-id="1"
onMouseEnter={this.handleMouseHover}
onMouseLeave={this.handleMouseHover}
>
Hover Me2
</div>
{this.state.isHovering && <div>{this.state.value}</div>}
</div>
);
}
}
ReactDOM.render(
<HoverExample />,
document.getElementById('root')
);
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>
You can pass the context text as shown in example. This is working code:
import React, { Component } from "react";
import { render } from "react-dom";
// Drive this using some configuration. You can set based on your requirement.
export const HOVER_Hello1 = "Hello1";
export const HOVER_Hello2 = "Hello2";
class HoverExample extends Component {
constructor(props) {
super(props);
this.handleMouseHover = this.handleMouseHover.bind(this);
this.state = {
isHovering: false,
contextText: ""
};
}
handleMouseHover = (e, currentText) => {
this.setState({
isHovering: !this.state.isHovering,
contextText: currentText
});
}
toggleHoverState(state) {
//
}
render() {
return (
<div>
<div
onMouseEnter={e => this.handleMouseHover(e, HOVER_Hello1)}
onMouseLeave={e => this.handleMouseHover(e, HOVER_Hello1)}
>
Hover Me
</div>
<div
onMouseEnter={e => this.handleMouseHover(e, HOVER_Hello2)}
onMouseLeave={e => this.handleMouseHover(e, HOVER_Hello2)}
>
Hover Me2
</div>
{this.state.isHovering && <div>{this.state.contextText}</div>}
</div>
);
}
}
export default HoverExample;
If the whole point is about linking dynamically messages to JSX-element you're hovering, you may store that binding (e.g. within an object).
Upon rendering, you simply pass some anchor (e.g. id property of corresponding object) within a custom attribute (data-*), so that later on you may retrieve that, look up for the matching object, put linked message into state and render the message.
Following is a quick demo:
const { Component } = React,
{ render } = ReactDOM,
rootNode = document.getElementById('root')
const data = [
{id:0, text: 'Hover me', message: 'Thanks for hovering'},
{id:1, text: 'Hover me too', message: 'Great job'}
]
class HoverableDivs extends Component {
state = {
messageToShow: null
}
enterHandler = ({target:{dataset:{id:recordId}}}) => {
const {message} = this.props.data.find(({id}) => id == recordId)
this.setState({messageToShow: message})
}
leaveHandler = () => this.setState({messageToShow: null})
render(){
return (
<div>
{
this.props.data.map(({text,id}) => (
<div
key={id}
data-id={id}
onMouseEnter={this.enterHandler}
onMouseLeave={this.leaveHandler}
>
{text}
</div>
))
}
{
this.state.messageToShow && <div>{this.state.messageToShow}</div>
}
</div>
)
}
}
render (
<HoverableDivs {...{data}} />,
rootNode
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>
As #CevaComic pointed out, you can do this with CSS. But if you want to use React, for example, because your actual problem is more complex, here is the answer.
You will need a way to tell apart the two elements. It could be done with some neat tricks, like setting an unique id to each element, passing a custom argument, or something else.
But I would advise against "cool tricks" as it's more difficult to understand what is going on, and the code is more prone to errors. I think the best way it to use a dumb approach of unique functions for unique elements.
Each onMouseEnter and onMouseLeave has to be an unique function (e.g. handleMouseHover1 and handleMouseHover2), and each of those functions need to control unique state (for example, isHovering1 and isHovering2). Then you have to render the element you want based on the state. Of course, for a real-world code, you will probably want to use more descriptive names to make the code more comprehensible. The full code would look something like this.
class HoverExample extends Component {
state = {
isHovering1: false,
isHovering2: false
};
handleMouseHover1 = () => {
this.setState(({ isHovering1 }) => ({ isHovering1: !isHovering1 }));
};
handleMouseHover2 = () => {
this.setState(({ isHovering2 }) => ({ isHovering2: !isHovering2 }));
};
render() {
const { isHovering1, isHovering2 } = this.state;
return (
<div>
<div
onMouseEnter={this.handleMouseHover1}
onMouseLeave={this.handleMouseHover1}
>
Hover Me1
</div>
<div
onMouseEnter={this.handleMouseHover2}
onMouseLeave={this.handleMouseHover2}
>
Hover Me2
</div>
{isHovering1 && <div>hello1</div>}
{isHovering2 && <div>hello2</div>}
</div>
);
}
}
Also, updated example: https://codesandbox.io/s/react-hover-example-rc3h0
Note: I have also edited the code to add some syntax sugar which exists with newer ECMAScript versions. Instead of binding the function, you can use the arrow function format, e.g. fn = () => { ... }. The arrow function means the this context is automatically bound to the function, so you don't have to do it manually. Also, you don't have to initialize this.state inside the constructor, you can define it as a class instance property. With those two things together, you do not need the constructor at all, and it makes the code a bit cleaner.
I want to make each element in a list clickable separately. I have an array of divs which I will loop into an array soon but for simplicity, I just hardcoded them into it(I am going to add more elements once I figure this out). When I click on the list item div, I want it to turn that Item into the text: "clicked".
I want to keep the files separate because this app will get big and I'm planning to add much more.
App.js
import React, { Component } from 'react';
import './App.css';
import Comp from './Comp';
class App extends Component {
state = {
list: [
"gameplay",
"visuals"
]
}
changetext = event =>{
this.setState({list: event.target.textContent = "clicked"});
}
render() {
return (
<div>
<Comp list = {this.state.list}
changetext = {this.changetext}/>
</div>
);
}
}
export default App;
Comp.js
The problem here is that when I click on a list item, The event.target.textContent is inputting {props.list[0]} and {props.list[1]} into the event object and turn both elements into c and l respectively.. both are the first and second elements in the string array "clicked".
The strange thing is, when I click the c or the l the second time, they act as I wanted them to and separately turn into clicking. So the question is, How can I achieve this without the initial hiccup? Let me know if you need set up information.
import React from 'react';
const Comp = props => {
let listarr = [];
listarr[0] = <div key = {0} onClick = {props.changetext}{props.list[0]}
listarr[1] = <div key = {1} onClick = {props.changetext}>{props.list[1]}
</div>
return(
<div>{listarr}</div>
);
}
export default Comp;
You have a couple of syntax errors. If you want to change the text to "clicked" you can do it like this:
const Comp = props => {
let listarr = [];
listarr.push(<div key={0} onClick={props.changetext}>{props.list[0]}</div>);
listarr.push(<div key={1} onClick={props.changetext}>{props.list[1]}</div>);
return (
<div>{listarr}</div>
);
}
class App extends Component {
state = {
list: [
"gameplay",
"visuals"
]
}
changetext = event => {
const { textContent } = event.target;
// Always use the callback syntax for setState when you need to refer to the previous state
this.setState(prevState => ({
list: prevState.list.map(el => textContent === el ? "clicked" : el)
}));
}
render() {
return (
<div>
<Comp list={this.state.list}
changetext={this.changetext} />
</div>
);
}
}
Just change the method you are passing as a property to be:
this.changetext.bind(this)
so it will look like this:
<div>
<Comp list = {this.state.list}
changetext = {this.changetext.bind(this)}/>
</div>
Or your other option could be to do this in the constructor:
constructor() {
super();
this.changetext.bind(this);
}
...
render() {
<div>
<Comp list = {this.state.list}
changetext = {this.changetext}/>
</div>
}
Original Question
I'm trying to render a list of items using React. The key is that the items share a common state, which can be controlled by each item.
For the sake of simplicity, let's say we have an array of strings. We have a List component that maps over the array, and generates the Item components. Each Item has a button that when clicked, it changes the state of all the items in the list (I've included a code snippet to convey what I'm trying to do).
I'm storing the state at the List component, and passing down its value to each Item child via props. The issue I'm encountering is that the button click (within Item) is not changing the UI state at all. I believe the issue has to do with the fact that items is not changing upon clicking the button (rightfully so), so React doesn't re-render the list (I would have expected some kind of UI update given the fact that the prop isEditing passed onto Item changes when the List state changes).
How can I have React handle this scenario?
Note: there seems to be a script error when clicking the Edit button in the code snippet, but I don't run into it when I run it locally. Instead, no errors are thrown, but nothing in the UI gets updated either. When I debug it, I can see that the state change in List is not propagated to its children.
Edited Question
Given the original question was not clear enough, I'm rephrasing it below.
Goal
I want to render a list of items in React. Each item should show a word, and an Edit button. The user should only be able edit one item at a time.
Acceptance Criteria
Upon loading, the user sees a list of words with an Edit button next to each.
When clicking Edit for item 1, only item 1 becomes editable and the Edit button becomes a Save button. The rest of the items on the list should no longer show their corresponding Edit button.
Upon clicking Save for item 0, the new value is shown for that item. All the Edit buttons (for the rest of the items) should become visible again.
Problem
On my original implementation, I was storing an edit state in the parent component (List), but this state wasn't properly being propagated to its Item children.
NOTE: My original implementation is lacking on the state management logic, which I found out later was the main culprit (see my response below). It also has a bind bug as noted by #Zhang below. I'm leaving it here for future reference, although it's not really a good example.
Here's my original implementation:
const items = ['foo', 'bar'];
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
isEditing: false
};
}
toggleIsEditing() {
this.setState((prevState) => {
return {
isEditing: !prevState.isEditing
}
});
}
render() {
return (
<ul>
{items.map((val) => (
<Item value={val}
toggleIsEditing={this.toggleIsEditing}
isEditing={this.state.isEditing}/>
))}
</ul>
);
}
}
class Item extends React.Component {
render() {
return (
<li>
<div>
<span>{this.props.value}</span>
{ !this.props.isEditing &&
(<button onClick={this.props.toggleIsEditing}>
Edit
</button>)
}
{ this.props.isEditing &&
(<div>
<span>...Editing</span>
<button onClick={this.props.toggleIsEditing}>
Stop
</button>
</div>)
}
</div>
</li>
);
}
}
ReactDOM.render(<List />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<body>
<div id="app" />
</body>
you didn't bind the parent scope when passing toggleIsEditing to child component
<Item value={val}
toggleIsEditing={this.toggleIsEditing.bind(this)}
isEditing={this.state.isEditing}/>
I figured out the solution when I rephrased my question, by rethinking through my implementation. I had a few issues with my original implementation:
The this in the non-lifecycle methods in the List class were not bound to the class scope (as noted by #ZhangBruce in his answer).
The state management logic in List was lacking other properties to be able to handle the use case.
Also, I believe adding state to the Item component itself was important to properly propagate the updates. Specifically, adding state.val was key (from what I understand). There may be other ways (possibly simpler), in which case I'd be curious to know, but in the meantime here's my solution:
const items = ['foo', 'bar'];
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
editingFieldIndex: -1
};
}
setEdit = (index = -1) => {
this.setState({
editingFieldIndex: index
});
}
render() {
return (
<ul>
{items.map((val, index) => (
<Item val={val}
index={index}
setEdit={this.setEdit}
editingFieldIndex={this.state.editingFieldIndex} />
))}
</ul>
);
}
}
class Item extends React.Component {
constructor(props) {
super(props);
this.state = {
val: props.val
};
}
save = (evt) => {
this.setState({
val: evt.target.value
});
}
render() {
const { index, setEdit, editingFieldIndex } = this.props;
const { val } = this.state;
const shouldShowEditableValue = editingFieldIndex === index;
const shouldShowSaveAction = editingFieldIndex === index;
const shouldHideActions =
editingFieldIndex !== -1 && editingFieldIndex !== index;
const editableValue = (
<input value={val} onChange={(evt) => this.save(evt)}/>
)
const readOnlyValue = (
<span>{val}</span>
)
const editAction = (
<button onClick={() => setEdit(index)}>
Edit
</button>
)
const saveAction = (
<button onClick={() => setEdit()}>
Save
</button>
)
return (
<li>
<div>
{ console.log(`index=${index}`) }
{ console.log(`editingFieldIndex=${editingFieldIndex}`) }
{ console.log(`shouldHideActions=${shouldHideActions}`) }
{
shouldShowEditableValue
? editableValue
: readOnlyValue
}
{
!shouldHideActions
? shouldShowSaveAction
? saveAction
: editAction
: ""
}
</div>
</li>
);
}
}
ReactDOM.render(<List />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<body>
<div id="app" />
</body>
I have a button in a child component for debugging purposes that prints the current state in its parent component. When it prints, that is how I want the state to be. When I hit another button in the same child component, the state changes in the parent, by removing a few properties.
Notice how the Id and SEO_CSEO_Survey_Questions__r properties are now missing.
Child Component
import React, { Component } from 'react';
import { Link } from 'react-router';
class Index extends Component {
constructor(props){
super(props)
this.state = {
selected: 'created'
}
this.updatePreview = this.updatePreview.bind(this);
this.renderEditArea = this.renderEditArea.bind(this);
}
isActive(value) {
return 'slds-tabs--default__item '+((value===this.state.selected) ?'slds-active':'');
}
updatePreview(e){
const updatedPreview = {
...this.props.selectedPreview,
[e.target.name]: e.target.value
};
this.props.update(updatedPreview)
}
// determines which type of edit area should display
// survey settings or question
renderEditArea() {
let selected = this.props.selectedPreview;
let hasNameKey = "Name" in selected;
if(hasNameKey){
return (
<div>
<input
onChange={(e) => this.updatePreview(e)}
name="Name"
type="text"
className="slds-input"
value={selected.Name ? selected.Name : ''}
/>
<input
onChange={(e) => this.updatePreview(e)}
name="SEO__Welcome_Text__c"
type="text"
className="slds-input"
value={selected.SEO__Welcome_Text__c ? selected.SEO__Welcome_Text__c : ''}
/>
</div>
)
}else {
return (
<input
onChange={(e) => this.updatePreview(e)}
name="SEO__Question__c"
type="text"
className="slds-input"
value={selected.SEO__Question__c ? selected.SEO__Question__c : ''}
/>
)
}
}
render() {
return (
<div className="slds-size--1-of-1 slds-medium-size--4-of-5 slds-large-size--4-of-5">
<div className="slds-tabs--default">
<h2 className="slds-text-heading--small slds-p-top--x-small" style={{position: "absolute"}}>
<button onClick={this.props.addQuestion} className="slds-button slds-button--icon slds-p-left--xx-small" title="add sur">
<svg className="slds-button__icon slds-button__icon--medium" aria-hidden="true">
<use xlinkHref={addIcon}></use>
</svg>
<span className="slds-assistive-text">Add Question</span>
</button>
</h2>
<ul className="slds-tabs--default__nav" style={{justifyContent: "flex-end"}}>
<Link to="/"className="slds-button slds-button--neutral">Back</Link>
<button onClick={this.props.save} className="slds-button slds-button--brand">Save</button>
<button onClick={this.props.currentState} className="slds-button slds-button--brand">Current State</button>
</ul>
</div>
<div className="slds-grid slds-wrap slds-grid--pull-padded">
{this.renderEditArea()}
</div>
</div>
);
}
}
export default Index;
Parent
import React, { Component } from 'react';
import { getQuestions, addQuestion, deleteQuestion, newSurveyQuestions, updateSurveyQuestions, EMPTY_SURVEY } from './helpers';
import SideBar from './survey/SideBar';
import MainArea from './survey/Index';
class Survey extends Component {
constructor(props) {
super(props)
this.state = {
survey: [],
selectedPreview: []
}
this.componentWillMount = this.componentWillMount.bind(this);
this.save = this.save.bind(this);
this.setSelectedPreview = this.setSelectedPreview.bind(this);
this.currentState = this.currentState.bind(this);
}
// if the url is `/survey/new`, create an empty survey
// to save for later.
// else if there is an id in the url, load the survey and questions
componentWillMount(){
if(this.props.pathname === "/survey/new") {
this.setState({
survey: EMPTY_SURVEY,
selectedPreview: EMPTY_SURVEY[0]
})
} else if (this.props.params.surveyId){
getQuestions(this.props.params.surveyId).then(survey => {
// 'survey' contains all the questions
this.setState({
survey,
selectedPreview: survey[0]
});
});
}
}
currentState() {
console.log('clicking Current State');
console.log(this.state.survey[0]);
}
// saves a new survey with associated newly created questions
// or saves an existing survey with associated questions
save() {
console.log('clicking Save');
console.log(this.state.survey[0]);
// if the url is set to survey/new
// save new survey with associated newly created questions
if(this.props.pathname === "/survey/new") {
newSurveyQuestions(this.state.survey).then( id => {
this.context.router.transitionTo(`/survey/id/${id}`);
})
// else update survey and questions
} else {
updateSurveyQuestions(this.state.survey);
}
}
// sets selectedPreview for the entire component and
// its children
setSelectedPreview(selectedPreview) {
this.setState({selectedPreview});
}
render() {
return (
<div className="slds-grid slds-wrap slds-grid--pull-padded">
<SideBar
survey={this.state.survey}
setSelectedPreview={this.setSelectedPreview}
deleteQuestion={this.deleteQuestion}
/>
<MainArea
addQuestion={this.addQuestion}
selectedPreview={this.state.selectedPreview}
update={this.update}
save={this.save}
currentState={this.currentState}
/>
</div>
);
}
}
Survey.contextTypes = {
router: React.PropTypes.object
}
export default Survey;
help function
export function updateSurveyQuestions(survey) {
// create proper url for update request
const requestURL = `${URL + survey[0].attributes.url}`;
// save questions for later update requests
const questions = survey[0].SEO__CSEO_Survey_Questions__r.records;
let s = [...survey];
// remove properties for proper body format
delete s[0].Id;
delete s[0].SEO__CSEO_Survey_Questions__r;
delete s[0].attributes;
axios.patch(requestURL, s[0], API_TOKEN);
questions.forEach( question => {
// save request url for later
let requestURL = `${URL + question.attributes.url }`;
// remove properites for proper body format
delete question.attributes;
delete question.Id;
axios.patch(requestURL, question, API_TOKEN);
})
}
When I removed all the code in save(), except for the console.log's, it prints as expected.
tl;dr: Your code is working fine. What you are seeing is a consequence of how objects and the console work. Do
console.log(this.state.survey[0].Id);
instead to see that the property does actually exist.
See also Is Chrome's JavaScript console lazy about evaluating arrays?
When I removed all the code in save(), except for the console.log's, it prints as expected.
That seems to suggest that that code is changing the object. E.g. updateSurveyQuestions or newSurveyQuestions might remove these properties.
You have to keep in mind that the output you see in the console is computed at the moment you expand the properties, not at the moment console.log is called. In fact, the little i icon next to Object tells you that. When you hover over it says:
Value below was evaluated just now
Here is a simplified example of what you are experiencing: Expand the object and notice that it is empty even though we removed it after calling console.dir (open your browser's console):
var obj = {foo: 42};
console.dir(obj);
delete obj.foo;
Ideally updateSurveyQuestions or newSurveyQuestions would not mutate the object directly, but rather clone it, make the necessary changes to the clone and then update the components state (if desired). E.g. a simple way to clone the object is via Object.assign:
var copy = Object.assign({}, this.state.survey[0]);