I am working on a quiz component, where user can appear for test. Questions are shown one after another to user and user checks the right answer.
But I am facing the below issue.
Description:
Checkbox does not uncheck for next question. It remains checked, once user click on any of the checkbox.
Steps:
1. Click on any checkbox options for the question.
2. Click on next for next question. [checkbox is checked from previous question]
[]2
Expected:
When next question appears, the checkbox should not be checked.
Actual:
When next questions appears, the checkbox is checked.
Code: On click of next, this component gets its data as a props from parent component.
// This component show one question at a time
import React from 'react';
import TextEditorDisplay from '../../texteditor/TextEditorDisplay';
import Form from 'react-bootstrap/Form';
class TestComponent extends React.PureComponent {
handleCheck = (e, idx) => {
console.log('inside handleCheck',e.target.value)
this.props.setAnswerGivenByUser(idx, e.target.checked);
};
render() {
return (
<div className="container">
<h3 className="quiz-question">
<TextEditorDisplay editorContent={this.props.quizQuestion.question} />
</h3>
<Form>
<table>
<tbody>
{this.props.quizQuestion.options && this.props.quizQuestion.options.map((option, idx) => (
<tr key={idx}>
<td>
<Form.Group controlId="formBasicCheckbox">
<Form.Check type="checkbox" value={option.data} onChange={e => this.handleCheck(e, idx)}/>
</Form.Group>
</td>
<td>
<p key={idx}>{option.data}</p>
</td>
</tr>
))}
</tbody>
</table>
</Form>
</div>
);
}
}
export default TestComponent;
Parent component:
import React from 'react';
import TestComponent from '../components/skill-assessment/users/TestComponent';
import Button from 'react-bootstrap/Button';
import api from '../services/remote/api';
class TestHomePage extends React.PureComponent {
x = 0;
y = 0;
arr = [];
constructor(props) {
super(props);
this.getQuizQuestionsAsPerLevel = this.getQuizQuestionsAsPerLevel.bind(this);
this.state = {
quizQuestion: [],
show: true,
options: [],
answers: []
};
}
getIdFromUrl = () => {
var url = this.props.location.pathname;
var splitUrl = url.split('/');
return splitUrl[2].toString();
};
componentDidMount() {
this.getQuizQuestionsAsPerLevel(1);
this.getQuizQuestionsAsPerLevel(2);
this.getQuizQuestionsAsPerLevel(3);
this.getQuizQuestionsAsPerLevel(4);
this.getQuizQuestionsAsPerLevel(5);
this.getQuizQuestionsAsPerLevel(6);
console.log('component did mount arr', this.arr);
}
getQuizQuestionsAsPerLevel(level) {
try {
api.getQuizQuestionsAsPerLevel({ id: this.getIdFromUrl(), level: level }).then(response => {
this.arr.push(response.data);
console.log('arr inside api', this.arr);
});
} catch (exception) {
console.log('exception', exception);
}
}
addUserQandA() {
try {
api.addUserQandA({
quizId: this.getIdFromUrl(),
quizQandA: [{ quizQuestionId: this.state.quizQuestion._id }, { answers: this.state.answers }]
}).then(response => {
console.log('add QandA response', response);
});
} catch (exception) {
console.log('exception', exception);
}
}
nextQuestion = () => {
// send prev Question data to QandA
if (this.state.quizQuestion && this.state.answers) {
this.addUserQandA();
}
if (this.x < this.arr.length - 1 && this.y >= this.arr[this.x].length) {
this.x = this.x + 1;
this.y = 0;
this.setState({ quizQuestion: this.arr[this.x][this.y], answers: [] });
} else if (this.x < this.arr.length && this.y < this.arr[this.x].length) {
this.setState({ quizQuestion: this.arr[this.x][this.y] });
this.y = this.y + 1;
} else {
// hide next button and highlight submit button
this.setState({ show: false });
}
};
setAnswerGivenByUser = (answerId, shouldAdd) => {
const answers = this.state.answers.slice();
if (shouldAdd) {
if (!answers.includes(answerId)) {
answers.push(answerId);
}
} else {
if (answers.includes(answerId)) {
const answerIndex = answers(a => a === answerId);
answers.splice(answerIndex, 1);
}
}
this.setState({ answers });
};
render() {
console.log('answers', this.state.answers);
return (
<div className="container">
<TestComponent quizQuestion={this.state.quizQuestion} setAnswerGivenByUser={this.setAnswerGivenByUser} />
{this.state.show && (
<Button variant="primary" onClick={this.nextQuestion}>
Next
</Button>
)}
<Button variant="primary">Submit</Button>
</div>
);
}
}
export default TestHomePage;
quiz Data Strcuture
Not refreshing problem was caused by not forced rerenderings.
For each levels <tr /> elements was rendered with numbered key always starting from 0. This way next level renders was besed on exiting nodes (updating), not rendered as new ones. First not changed node (in sense of the same props) stops deeper analisys. In this case it stops on <Form.Group controlId="formBasicCheckbox"> - it's child is not updated even when option.data differs.
Solution
key is used for distinguish nodes rendered in loops. key should be unique. It should not be a number only ... they should be always unique.
Simple fix is to use additionally passed prop level:
<tr key={this.props.level * 10 + idx} />
In fact ... as <p key={idx}>{option.data}</p> was updated ... it should be enough to use this (or similar) unique key/prop for <Form.Group/> (f.e. controlId). Using unique on <tr/> level we're forcing render of a new structure (can be costly in some scenarios).
Related
I tried to "2. Adding a class of "error" to a row will highlight it red. Provide this
visual feedback on rows which have duplicate ranks selected." You can view my code at js_react_problem_after folder under https://github.com/HuydDo/plm_challenge. Please let me know if you have any questions. Thanks,
MainPage.jsx
import React from 'react';
import _ from 'lodash';
import FormRow from './FormRow.jsx';
import Animal from './Animal.js';
class MainPage extends React.Component {
constructor(props) {
super(props);
this.state = {
animals: ['panda','cat','capybara','iguana','muskrat'].map((name) => {
return new Animal(name);
//instantiating an object of that class. animal is a class that has
//a constructor that takes in one variable which is name and it's
//creating a bunch of objects which are instances of that class
}),
error: 'Ranks must be unique',
errorFlag: false
};
}
render() {
// console.log('this.state.animal',this.state.animals)
const errCount = {}
// this.state.animals.forEach((animal) => {
// if (animal.rank && errCount[animal.rank]){
// errCount[animal.rank] += 1
// return
// }
// errCount[animal.rank] = 1
// })
const rows = this.state.animals.map((animal) => {
// console.log(animal)
// if (animal.rank && errCount[animal.rank]){
// errCount[animal.rank] += 1
// return
// }
// errCount[animal.rank] = 1
// for (let prop in errCount){
// if(errCount[prop] >=2 && prop){
// // this.setState({
// // errorFlag : true
// // })
// }
// }
return (
<FormRow
// errorFlag={this.state.errorFlag}
animalRank={animal.rank}
selectButton={(updateRank) => {
animal.setRank(updateRank)
this.setState({animal: this.state.animals})
}}
animalName={animal.name}
key={animal.name}
/>
);
});
//create headers
const headers = _.range(1, 6).map((i) => <th key={`header-${i}`}>{i}</th>);
let disabled = false
let error = false
const count = {}
this.state.animals.forEach((animal) => {
if(!animal.rank){ //disable the button if any rank is not selected
disabled = true
}
if (animal.rank && count[animal.rank]){
count[animal.rank] += 1
return
}
count[animal.rank] = 1
})
for (let prop in count){
if(count[prop] >=2 && prop){
error = true
disabled = true
// console.log(prop + " counted " + count[prop] + " times")
}
}
// console.log('disabled:', disabled, 'error:', error)
// console.log(count)
return (
<div>
<table>
<thead>
<tr>
<th></th>
{headers}
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
<div>{error ? this.state.error : null}</div>
<input type="submit" disabled={disabled} />{/*disabled is a prop of the input tag*/}
</div>
);
}
}
export default MainPage;
FormRow.jsx
import React from 'react';
import _ from 'lodash';
/**
* TODO: 2. Adding a class of "error" to a row will highlight it red. Provide this visual feedback on rows which have duplicate ranks selected.
* TODO: 3. There is a place to display an error message near the submit button. Show this error message: `Ranks must be unique` whenever the user has selected the same rank on multiple rows.
*/
class FormRow extends React.Component {
constructor(props){
super(props)
this.state = {
rank: this.props.animalRank
}
}
//*if we receive new props and the animal props
//*doesn't match whats inside the component then
//*we should update the component
componentDidUpdate(){
if(this.props.animalRank !== this.state.rank){
this.setState({rank: this.props.animalRank})
// console.log(`rank is updated to: ${this.state.rank}`)
}
}
render() {
//created array 1-6, iterating over it,
// and for each number in that array creating
// a new array and returning this HTML chunk
const cells = _.range(1, 6).map((i) => {
return (
<td key={`${this.props.animalName}-${i}`}>
<input
onClick={() => {
// i is the number clicked on
this.props.selectButton(i)
}}
type="radio"
name={this.props.animalName}
value={i}
/>
</td>
);
});
// console.log(this.props)
const done = this.state.rank ? "done" : null
// console.log(done)
return (
<tr className={done}>
<th>{this.props.animalName}</th>
{cells}
</tr>
)
}
}
export default FormRow;
Here is a solution. In future questions, please try to limit code instead of wholesale copying everything with commented code included.
const trProps = {}
if(error){
trProps.className= "errorClass"
}
else if(this.state.rank){
trProps.className = "done"
}
<tr {...trProps}>
https://reactjs.org/docs/jsx-in-depth.html#spread-attributes
I am creating an app, where users should compose a correct sentence from shuffled one. Like on the following picture. The problem is that I am not implementing the app in a right way. For example, I do not change inputs through state. And my main question is how to clear input of a particular line (a bunch of inputs) ? I need it when a user types something wrong and needs to start again. I know that input field should be controlled through state, but it is impossible since all inputs are generated according to number of words. And in one line there maybe more than one input. Let me explain how my code works, so that you have an idea my implementation.
This is where I get data from.
export default {
id:'1',
parts:[
{
speaker:'Speaker1',
words:'Hello how are you?'
},
{ speaker:'Speaker2',
words:'I am OK, thanks'
},
{ speaker:'Speaker1',
words:'What are your plans for Saturday?'
}
]
}
import React, { Component } from 'react';
import {CopyToClipboard} from 'react-copy-to-clipboard';
import { MdDoneAll } from "react-icons/md";
// This is the array where data from inputs is pushed to
var pushArr = [];
// Initial points
var points = 0;
class DialogueShuffleFrame extends Component {
constructor(props) {
super(props)
this.state = {
// Variable to show correct or incorrect notification
showCorrect:false
}
this.writeSometihng = this.writeSometihng.bind(this)
}
// Function that is triggered when a tick is clicked
// Pushes value to the pushArr array when onBlur function is triggered
writeSometihng(e) {
e.preventDefault()
pushArr.push(e.target.value)
console.log(pushArr)
}
// Function check if array of value from input matches to an array from
// initial data source
checkLines(arr, lines) {
let joinedStr = arr.join(' ');
//console.log(joinedStr);
lines[0].parts.map((obj) => {
let line = obj.words
if (joinedStr === line) {
this.setState({
showCorrect:true
})
pushArr = [];
points += 80;
} else {
pushArr = [];
}
})
}
// Resets pushArr array
reset() {
pushArr.length = 0
console.log('clicked')
}
// Shuffles words
formatWords(words) {
const splittedWords = words.split(' ')
const shuffledArray = this.shuffle(splittedWords)
return (
shuffledArray.map((word, index) => (
<>
<input className="word-to-drop-input" id={index} onBlur={this.writeSometihng} size={2} />
<CopyToClipboard text={word}>
<span key={uid(word)} value={word} className="word-to-drop">{word}</span>
</CopyToClipboard>
</>
))
)
}
shuffle(a) {
var j, x, i;
for (i = a.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
x = a[i];
a[i] = a[j];
a[j] = x;
}
return a;
}
render() {
const {lines} = this.props
const shuffles = lines[0].parts && (
lines[0].parts.map((element,i) => (
<>
<li className="line" key={i}><span>{element.speaker}{": "}</span><span>{this.formatWords(element.words)}</span></li>
<MdDoneAll type="button" onClick={() => {this.checkLines(pushArr, lines)}} style={{color:'white'}}/>
</>
))
)
return (
<>
<h1 className="centered" style={{color:'white'}}>Dialogue shuffle frame</h1>
<ul className="lines-container">
{shuffles}
</ul>
{<div className="reactangular">{this.state.showCorrect ? 'Correct' : 'Incorrect'}</div>}
<div>{points}</div>
<div className="reactangular" onClick={() => this.reset()}>Reset</div>
</>
)
}
}
export default DialogueShuffleFrame;
I'd recommend to use proper data structure with state management, but that's a different story.
To solve your specific issue of clearing input elements, you can use ReactDOM.findDOMNode to access the DOM node and traverse the input elements and set the value to empty string.
Something like this:
class App extends React.Component {
check = (ref) => {
const inputElements = ReactDOM.findDOMNode(ref.target).parentNode.getElementsByTagName('input');
[...inputElements].forEach(el => el.value = '')
}
render() {
return (
<div>
<ul>
<li>
<input />
<input />
<button onClick={this.check}>Check</button>
</li>
<li>
<input />
<input />
<input />
<button onClick={this.check}>Check</button>
</li>
</ul>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
I have a list of checkboxes that receive a handler function, which changes the state when a checkbox is checked/unchecked. When I click on the checkbox, I am getting the following error:
TypeError: onChangePoll is not a function
I've tried:
Changing the handler function to an arrow function
Moving the checkbox component from it's modular file to place it inside the file where the function resides
Changed onChange to onClick
Traced it through the DOM as it renders
It seems that the handler function is being passed to the component correctly when rendering, but when I make the click, it seems to disappear.
For the code below, affiliates is an array that looks like this:
[
{
shortName: `a`
},
{
shortName: `b`
},
{
shortName: `c`
}
];
NewPoll.js
import React, { useState } from 'react';
import { Redirect } from "react-router";
import { AffiliateSelects } from './../helpers/FormElements';
import { affiliates } from './../helpers/Config';
import { initialNewPoll } from './../helpers/Config';
import { isAffiliates, validateNewPollModal } from './../helpers/FormValidator';
function NewPoll(props) {
const {
pollData,
type,
onChangePoll
} = props;
// Set headline for modal
const headline = type === `new` ? `Add a New Poll` : `Edit “${pollData.pollName.value}”`;
return(
<div className="modal-content">
<h2>{headline}</h2>
<form className="form__inputs form__inputs--modal">
<div className="form__inputs-row">
<label
className={pollData.affiliates.error ? `form__input-label form__input-label--error` : `form__input-label`}
htmlFor="affiliates"
>
{pollData.affiliates.error ? `Affiliates (${pollData.affiliates.error})` : `Affiliates`}
</label>
<AffiliateSelects
type="checkbox"
name="affiliates"
state={pollData.affiliates.value}
onChangePoll={onChangePoll}
/>
</div>
<div className="form__buttons-row">
<ul className="form__buttons-row-list">
<li className="form__buttons-row-item">
<button
className="button"
type="reset"
name="clear-filters"
onClick={(e) => onChangePoll({
name: `clearFields`
})}
>
Clear Fields
</button>
</li>
<li className="form__buttons-row-item">
<button
className="button"
type="reset"
name="create-poll"
onClick={(e) => onChangePoll({
name: `createPoll`
})}
>
{type === `new` ? `Create Poll` : `Save Edits`}
</button>
</li>
</ul>
</div>
</form>
</div>
)
}
export const NewPollModal = (props) => {
const {
pollData,
setPollData,
type
} = props;
const onChangePoll = (el) => {
// Validate the poll data being sent through
let validPoll;
switch(el.name) {
case `affiliates`:
validPoll = isAffiliates(el.value, pollData.affiliates.value);
break;
case `clearFields`:
validPoll = initialNewPoll;
break;
default:
break;
}
if (el.name === `createPoll`) {
// Check to make sure all of the fields are valid
const isPollValid = validateNewPollModal(pollData);
if (isPollValid.valid) {
// If they are valid, send the state to the poll edit page
setPollData({
...pollData,
valid: true
});
} else {
// If they are not valid, create errors on the form
setPollData(isPollValid.validPoll);
}
} else if (el.name === `clearFields`) {
setPollData(validPoll);
} else {
setPollData({
...pollData,
[`${el.name}`]: {
valid: validPoll.valid,
error: validPoll.error,
value: validPoll.value
}
});
}
}
if (pollData.valid) {
return(
<Redirect
to={{
pathname: "/poll",
state: {
affiliates: pollData.affiliates.value
}
}}
/>
)
} else {
return(
<NewPoll
pollData={pollData}
type={type}
onChangePoll={onChangePoll}
/>
)
}
}
FormElements.js
import React from 'react';
import { affiliates } from './Config';
function ListItem(props) {
const {
aff,
type,
state,
onChangePoll
} = props;
// If we are in edit mode, add the checkmark if the affiliate is in state
const checked = state.includes(aff) ? true : false;
const affiliates = {
name: `affiliates`,
value: aff
};
if (type === `checkbox`) {
console.log(onChangePoll);
return(
<li className="affiliate-selects__items">
<input
className="affiliate-selects__checkbox"
type={type}
name="affiliates"
id={`affiliates-${aff}`}
checked={checked}
onChange={() => onChangePoll(affiliates)}
/>
<label className="affiliate-selects__label" htmlFor={`affiliates-${aff}`}>{aff}</label>
</li>
)
} else if (type === `list`) {
return(
<li className="affiliate-selects__items">{aff}</li>
)
}
}
function List(props) {
const {
type,
state,
affList,
onChangePoll
} = props;
let listClass;
if (type === `checkbox`) {
listClass = `affiliate-selects affiliate-selects--checkbox`;
} else if (type === `list`) {
listClass = `affiliate-selects affiliate-selects--list`;
}
return(
<ul className={listClass}>
{affList.map((aff) =>
<ListItem
key={`affiliate-selects-${aff.shortName}`}
aff={aff.shortName}
type={type}
state={state}
onChangePoll={onChangePoll}
/>
)}
</ul>
)
}
/**
* Displays a list of affiliates, eiter as a checkbox list, or as a regular list.
* #param {String} props.type Will display the affiliates as a list of checkboxes for a display * list
* Ex: `checkbox` || `list`
* #param {Function} props.handleOnChange Event handler function that changes the items that are filtered
* (optional)
* #param {String} props.prefix Will prefix all input IDs so form inputs will work correctly
* (optional)
* #param {Array} props.affArray Array of affiliate string names
* Ex: [`nj`, `oregonlive`, `masslive`]
* (optional)
*/
export function AffiliateSelects(props) {
const type = props.type || `list`;
const state = props.state || [];
const {
affArray,
onChangePoll
} = props;
// If the user doens't pass in an array, list all of the affiliates from the config
let affList;
if (affArray === undefined || affArray.length === 0) {
affList = affiliates;
} else {
affList = affArray.reduce((accum, aff) => {
accum.push({shortName: aff});
return accum;
}, []);
}
return(
<List
type={type}
state={state}
affList={affList}
onChangePoll={onChangePoll}
/>
)
}
The expected behavior would be that the user clicks the checkbox next to an affiliate, and that affiliate gets validated by the handler and saved to state.
Newbie React question here on show hide functionality.
I have a state of 'show' that I set to false:
this.state = {
show: false,
};
Then I use the following function to toggle
toggleDiv = () => {
const { show } = this.state;
this.setState({ show : !show })
}
And my display is
{this.state.show && <xxxxxx> }
This all works fine. However I want to apply the function it to multiple cases (similar to accordion, without the closing of other children. So I change my constructor to
this.state = {
show: [false,false,false,false,false,false]
};
and this to recognise there are 6 different 'shows'.
{this.state.show[0] && <xxxxxx> }
{this.state.show[1] && <xxxxxx> } etc
But where I get stuck is how to account for them in my toggleDiv function. How do I insert the square bracket reference to the index of show (if this is my problem)?
toggleDiv = () => {
const { show } = this.state;
this.setState({ show : !show })
}
Thanks for looking.
First of all I'd suggest you not to rely on current state in setState function, but to use the callback option to be 100% sure that you are addressing to the newest state:
this.setState((prevState) => ({ show: !prevState.show }));
How to deal with multiple elements?
You'll have to pass the index of currently clicked element.
{yourElements.map((elem, i) => <YourElem onClick={this.toggleDiv(i)} />)}
and then inside your toggleDiv function:
toggleDiv = (i) => () => {
this.setState((prevState) => {
const r = [...prevState.show]; // create a copy to avoid state mutation
r[i] = !prevState.show[i];
return {
show: r,
}
}
}
Use an array instead of a single value. In your toggle div function make a copy of the state array make necessary changes and push the entire array back up to state at the end.
This is some simplified code showing the workflow I described above
export default class myClass extends React.Component{
constructor(props){
super(props);
this.state = { show: new Array(2).fill(false) };
}
//you need a index or id to use this method
toggleDiv = (index) => {
var clone = Object.assign( {}, this.state.show ); //ES6 Clones Object
switch(clone[index]){
case false:
clone[index] = true
break;
case true:
clone[index] = false
break;
}
this.setState({ show: clone });
}
render(){
return(
<div>
{ this.state.show[0] && <div> First Div </div> }
{ this.state.show[1] && <div> Second Div </div> }
{ this.state.show[2] && <div> Third Div </div> }
</div>
)
}
}
I'm mostly a C++ developer and I'm having a hard time understanding how can I create something as a 'Delegate' in React.
What I wanna achieve: Pass a custom component to a Table Component that has the required code to edit the data on a table cell correctly.
on my mainApp:
<TableComponent
headerData=["first", "second", "third"]
rowEditDelegates=[<input/>, <button></button>, <combobox/>]
/>
The code is much shorter for brievety.
class TableComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
editDelegates = this.props.rowEditDelegates;
displayEditors = false;
}
onEditToogled() {
/* Here I have no idea how to get the editDelegates
and pass data to it. */
setState({displayEditors : true});
}
}
render() {
let row = null;
if(this.state.displayEditors) {
row = this.state.editDelegates.map((item) => <td> {item} </td>)
} else {
row = this.props.bodyData.map((item) => <td> {item} </td>)
}
}
};
I cannot access the delegate's methods because it's a rendered component, and I didn't understand how to work with a component "pointer" (I don't even know if it exists), maybe my problem needs a different mindset than a c++ programmer one.
You have couple of options here:
1) Use cloneElement function:
onEditToogled()
{
setState({displayEditors : true});
}
render()
{
let row = null;
if(this.state.displayEditors)
{
row = this.state.editDelegates.map((item) => {
var alteredElement = React.cloneElement(item, {className: "newPropertyValue"});
return (<td> {alteredElement} </td>);
});
}
else
{
row = this.props.bodyData.map((item) => <td> {item} </td>)
}
}
2) Change the way how you pass the editor components, for example:
<TableComponent
headerData=["first", "second", "third"]
rowEditDelegates=[(props) => <input {...props}/>, (props) => <button {...props}/>, (props) => <combobox {...props}/>]
/>
And later:
render() {
let row = null;
if(this.state.displayEditors) {
row = this.state.editDelegates.map((itemFunc) => <td> {itemFunc({className: "newPropertyValue"})} </td>)
} else {
row = this.props.bodyData.map((itemFunc) => <td> {itemFunc()} </td>)
}
}
As a side note.
I do not think there is a need for you to copy and keep your "delegates" in state. They are unlikely to change? If so - just leave them in props.