So this is a form where the user can add sections to add a question (to build a quiz) and I notice that when I fill in the Answer Choices
and put a file in my dropZone
(drop works but doesn't update correctly, you can ignore this) the Answer Choices
and dropZone
rerender and the fields like refresh and become empty.
I am not entirely sure why this is happening, i have tried looking at similar issues but I couldn't get it to work. Here is my CodeSandbox with my App.
I am thinking that it may be the addQuestion
function in my Questions
component. Here's the code for that:
addQuestion = question => {
questionIdx++;
var newQuestion = { uniqueId: uuid(), question: "" }
this.setState(prevState => ({
questions: [...prevState.questions, newQuestion]
}));
return { questions: newQuestion }
};
I am new to React and Js so any tips/explanations will do loads of help. Thanks!
When you add a new question, and update the Questions
components state variable questions
(Array type), the whole component (Questions
and its children) goes through an update process where it recalculates the output DOM tree (a virtual DOM) based on the new state and compares it to the pre-state-change virtual DOM. Before anything is 'rerendered' it checks to see if the virtual DOMs of the two versions are different among other things, if they are it will rerender the parts that change as efficiently as possible.
In your case, the recalculation sees many Answers
component inside of many Question
and since Answers
doesn't have any props, it is basically a fresh render to its initial state which includes 4 empty inputs. The simplest solution I can think of is in the state of the Questions
component, make sure each object in the this.state.questions
Array has an answers
attribute (of type Array of Objects). In the addQuestion
method modify var newQuestion = { uniqueId: uuid(), question: "" };
to includes this data on the answers related to that question.
Then when rendering each individual question, pass this answer data (Array of answers) as a prop to Answers
and in turn to each individual Answer
component based on the index (an Object or a String). At the same time, you must pass an updateAnswers
method as a prop to Question
that in turn is passed to Answers
and Answer
, that is called when an Answer
s input field is changed. The question id and the id of the answer needs to be passed to this method to ultimately modify the answer data that should be now stored in the state of the Questions
component. I adjusted the code from the sandbox below to work along these lines, although I didn't make sure to cleanup all the damage:
import React, { Component } from "react";
import "./App.css";
var uuid = require("uuid-v4");
// Generate a new UUID
var myUUID = uuid();
// Validate a UUID as proper V4 format
uuid.isUUID(myUUID); // true
class DropZone extends Component {
constructor(props) {
super(props);
this.state = {
file: "",
fileId: uuid(),
className: "dropZone"
};
this.handleChange = this.handleChange.bind(this);
this._onDragEnter = this._onDragEnter.bind(this);
this._onDragLeave = this._onDragLeave.bind(this);
this._onDragOver = this._onDragOver.bind(this);
this._onDrop = this._onDrop.bind(this);
}
handleChange(file = "") {
this.setState({
file: URL.createObjectURL(file)
});
//document.getElementsByClassName("dropZone").style.backgroundImage = 'url(' + this.state.file + ')';
}
componentDidMount() {
window.addEventListener("mouseup", this._onDragLeave);
window.addEventListener("dragenter", this._onDragEnter);
window.addEventListener("dragover", this._onDragOver);
document
.getElementById("dragbox")
.addEventListener("dragleave", this._onDragLeave);
window.addEventListener("drop", this._onDrop);
}
componentWillUnmount() {
window.removeEventListener("mouseup", this._onDragLeave);
window.removeEventListener("dragenter", this._onDragEnter);
window.addEventListener("dragover", this._onDragOver);
document
.getElementById("dragbox")
.removeEventListener("dragleave", this._onDragLeave);
window.removeEventListener("drop", this._onDrop);
}
_onDragEnter(e) {
e.stopPropagation();
e.preventDefault();
return false;
}
_onDragOver(e) {
e.preventDefault();
e.stopPropagation();
return false;
}
_onDragLeave(e) {
e.stopPropagation();
e.preventDefault();
return false;
}
_onDrop(e, event) {
e.preventDefault();
this.handleChange(e.dataTransfer.files[0]);
let files = e.dataTransfer.files;
console.log("Files dropped: ", files);
// Upload files
console.log(this.state.file);
return false;
}
render() {
const uniqueId = this.state.fileId;
return (
<div>
<input
type="file"
id={uniqueId}
name={uniqueId}
class="inputFile"
onChange={e => this.handleChange(e.target.files[0])}
/>
<label htmlFor={uniqueId} value={this.state.file}>
{this.props.children}
<div className="dropZone" id="dragbox" onChange={this.handleChange}>
Drop or Choose File
<img src={this.state.file} id="pic" name="file" accept="image/*" />
</div>
</label>
<div />
</div>
);
}
}
class Answers extends Component {
constructor(props) {
super(props);
this.state = {
answers: props.answers,
};
this.handleUpdate = this.handleUpdate.bind(this);
}
// let event = {
// index: 1,
// value: 'hello'
// };
handleUpdate(event) {
//if ("1" == 1) // true
//if ("1" === 1) //false
// var answers = this.state.answers;
// answers[event.index] = event.value;
// this.setState(() => ({
// answers: answers
// }));
var answers = this.state.answers.slice();
for (var i = 0; i < answers.length; i++) {
if (answers[i].answerId == event.answerId) {
answers[i].answer = event.value;
break;
}
}
this.setState(() => ({
answers: answers
}));
this.props.updateAnswers(answers)
console.log(event);
}
render() {
return (
<div id="answers">
Answer Choices<br />
{this.state.answers.map((value, index) => (
<Answer
key={`${value}-${index}`}
onUpdate={this.handleUpdate}
value={value}
number={index}
name="answer"
/>
))}
</div>
);
}
}
class Answer extends Component {
constructor(props) {
super(props);
this.state = {
answer: props.value.answer,
answerId: props.value.answerId,
isCorrect: props.value.isCorrect,
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
const target = event.target;
const value = target.type === "checkbox" ? target.checked : target.value;
this.setState({
answer: value
});
this.props.onUpdate({
answerId: this.state.answerId,
value
});
// let sample = {
// kyle: "toast",
// cam: "pine"
// };
// sample.kyle
// sample.cam
}
render() {
return (
<div>
<input type="checkbox" />
<input
type="text"
value={this.state.answer}
onChange={this.handleChange}
key={this.state.answerId}
name="answer"
/>
{/*console.log(this.state.answerId)*/}
</div>
);
}
}
var questionIdx = 0;
class Questions extends Component {
constructor(props) {
super(props);
this.state = {
questions: []
};
this.handleUpdate = this.handleUpdate.bind(this);
this.handleUpdate = this.handleUpdate.bind(this);
this.removeQuestion = this.removeQuestion.bind(this);
}
handleUpdate(event) {
//if ("1" == 1) // true
//if ("1" === 1) //false
var questions = this.state.questions.slice();
for (var i = 0; i < questions.length; i++) {
if (questions[i].uniqueId == event.uniqueId) {
questions[i].question = event.value;
break;
}
}
this.setState(() => ({
questions: questions
}));
console.log(event, questions);
}
updateAnswers(answers, uniqueId) {
const questions = this.state.questions
questions.forEach((question) => {
if (question.uniqueId === uniqueId) {
question.answers = answers
}
})
this.setState({
questions,
})
}
addQuestion = question => {
questionIdx++;
var newQuestion = {
uniqueId: uuid(),
question: "",
answers: [
{ answer: "", answerId: uuid(), isCorrect: false,},
{ answer: "", answerId: uuid(), isCorrect: false,},
{ answer: "", answerId: uuid(), isCorrect: false,},
{ answer: "", answerId: uuid(), isCorrect: false,}]
}
this.setState(prevState => ({
questions: [...prevState.questions, newQuestion]
}));
return { questions: newQuestion };
};
removeQuestion(uniqueId, questions) {
this.setState(({ questions }) => {
var questionRemoved = this.state.questions.filter(
props => props.uniqueId !== uniqueId
);
return { questions: questionRemoved };
});
console.log(
"remove button",
uniqueId,
JSON.stringify(this.state.questions, null, " ")
);
}
render() {
return (
<div id="questions">
<ol id="quesitonsList">
{this.state.questions.map((value, index) => (
<li key={value.uniqueId}>
{
<RemoveQuestionButton
onClick={this.removeQuestion}
value={value.uniqueId}
/>
}
{
<Question
onUpdate={this.handleUpdate}
value={value}
number={index}
updateAnswers={(answers) => this.updateAnswers(answers, value.uniqueId) }
/>
}
{<br />}
</li>
))}
</ol>
<AddQuestionButton onClick={this.addQuestion} />
</div>
);
}
}
class Question extends Component {
constructor(props) {
super(props);
this.state = {
question: props.value.question,
uniqueId: props.value.uniqueId,
answers: props.value.answers,
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
const target = event.target;
const value = target.type === "checkbox" ? target.checked : target.value;
this.setState({
question: value
});
this.props.onUpdate({
uniqueId: this.state.uniqueId,
value
});
}
render() {
return (
<div id={"questionDiv" + questionIdx} key={myUUID + questionIdx + 1}>
Question<br />
<input
type="text"
value={this.state.question}
onChange={this.handleChange}
key={this.state.uniqueId}
name="question"
/>
<DropZone />
<Answers updateAnswers={this.props.updateAnswers} answers={this.state.answers} />
</div>
);
}
}
class IntroFields extends Component {
constructor(props) {
super(props);
this.state = {
title: "",
author: ""
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
const target = event.target;
const value = target.type === "checkbox" ? target.checked : target.value;
const name = target.name;
console.log([name]);
this.setState((previousState, props) => ({
[name]: value
}));
}
render() {
return (
<div id="IntroFields">
Title:{" "}
<input
type="text"
value={this.state.title}
onChange={this.handleChange}
name="title"
/>
Author:{" "}
<input
type="text"
value={this.state.author}
onChange={this.handleChange}
name="author"
/>
</div>
);
}
}
class AddQuestionButton extends Component {
addQuestion = () => {
this.props.onClick();
};
render() {
return (
<div id="addQuestionButtonDiv">
<button id="button" onClick={this.addQuestion} />
<label id="addQuestionButton" onClick={this.addQuestion}>
Add Question
</label>
</div>
);
}
}
class RemoveQuestionButton extends Component {
removeQuestion = () => {
this.props.onClick(this.props.value);
};
render() {
return (
<div id="removeQuestionButtonDiv">
<button id="button" onClick={this.removeQuestion} key={uuid()} />
<label
id="removeQuestionButton"
onClick={this.removeQuestion}
key={uuid()}
>
Remove Question
</label>
</div>
);
}
}
class BuilderForm extends Component {
render() {
return (
<div id="formDiv">
<IntroFields />
<Questions />
</div>
);
}
}
export default BuilderForm;