Here's a dynamic form for student details where new student fields can be added or deleted, using the '+' & the 'X' buttons. (i.e, number of student fields is decided by user). Here, App => Parent Component & StudentsFormElement => Child Component.
Problem: Any of the 'X' buttons, when clicked, are deleting only the last student field(in DOM) and not the one which was supposed to be deleted. But most importantly, the parent component state changes correctly, with the correct student details being deleted from it. This state change is not being reflected in the DOM.
codesandbox: https://codesandbox.io/s/peaceful-spence-1j3nv?fontsize=14&hidenavigation=1&theme=dark
App component:
class App extends React.Component {
constructor(props) {
super(props)
let studentsFormElementsTemp = []
let tempSTUDENTS = {0: ["", ""]}
this.state = {
STUDENTS: tempSTUDENTS
}
studentsFormElementsTemp.push(<StudentsFormElement id="0" student={this.state.STUDENTS[0]} onStudentsChange={this.onStudentsChange} />)
this.state = {
studentsFormElements: studentsFormElementsTemp,
studentsElementsIdArray: [0],
STUDENTS: tempSTUDENTS
}
}
render() {
return (
<div>
<h2 style={{textAlign: "center", display: "inline-block"}}>Students</h2><Button id="+" style={{display: "inline-block"}} variant="success" onClick={this.onStudentsChange}>+</Button>
<form>
{this.state.studentsFormElements}
</form>
<p>{JSON.stringify(this.state.STUDENTS)}</p>
</div>
)
}
onStudentsChange = (e) => {
if (e.target.name === "studentId" || e.target.name === "studentName") { //HANDLING TYPED CHARACTERS.
let tempSTUDENTS = this.state.STUDENTS
if (e.target.name === "studentId") {
tempSTUDENTS[e.target.id][0] = e.target.value
}
else {
tempSTUDENTS[e.target.id][1] = e.target.value
}
this.setState({
STUDENTS: tempSTUDENTS
})
} else {
let studentsFormElementsTemp = this.state.studentsFormElements
let studentsElementsIdArrayTemp = this.state.studentsElementsIdArray
let tempSTUDENTS = this.state.STUDENTS
if (e.target.id === "+") { //ADDING (+) STUDENT
tempSTUDENTS[studentsElementsIdArrayTemp[studentsElementsIdArrayTemp.length - 1] + 1] = ["", ""]
this.setState({
STUDENTS: tempSTUDENTS
})
studentsFormElementsTemp.push(<StudentsFormElement id={studentsElementsIdArrayTemp[studentsElementsIdArrayTemp.length - 1] + 1} student={this.state.STUDENTS[studentsElementsIdArrayTemp[studentsElementsIdArrayTemp.length - 1] + 1]} onStudentsChange={this.onStudentsChange} />)
studentsElementsIdArrayTemp.push(studentsElementsIdArrayTemp[studentsElementsIdArrayTemp.length - 1] + 1)
this.setState({
studentsFormElements: studentsFormElementsTemp,
studentsElementsIdArray: studentsElementsIdArrayTemp
})
} else { //DELETING STUDENT (X)
let studentIndex = studentsElementsIdArrayTemp.indexOf(parseInt(e.target.id))
studentsFormElementsTemp.splice(studentIndex, 1)
studentsElementsIdArrayTemp.splice(studentIndex, 1)
delete tempSTUDENTS[e.target.id]
this.setState({
studentsFormElements: studentsFormElementsTemp,
studentsElementsIdArray: studentsElementsIdArrayTemp,
STUDENTS: tempSTUDENTS
})
}
}
}
}
StudentsFormElement component:
class StudentsFormElement extends React.Component {
render() {
return (
<InputGroup className="mb-3">
<FormControl name="studentId" id={this.props.id} defaultValue={this.props.student[0]} placeholder="Id" onChange={this.props.onStudentsChange} />
<FormControl name="studentName" id={this.props.id} defaultValue={this.props.student[1]} placeholder="Name" onChange={this.props.onStudentsChange} />
<InputGroup.Append style={{display: "inline-block"}}>
<Button id={this.props.id} variant="danger" onClick={this.props.onStudentsChange}>X</Button>
</InputGroup.Append>
</InputGroup>
)
}
}
Things I've tried: I've tried this.forceUpdate() just after handling 'X' button in onStudentsChange() but it doesn't make a difference.
Again, codesandbox: https://codesandbox.io/s/peaceful-spence-1j3nv?fontsize=14&hidenavigation=1&theme=dark
You have to add a key
prop to StudentsFormElement
. If you open the console you can see React throwing an error. I made two changes,
studentsFormElementsTemp.push(<StudentsFormElement id="0" key={0} student={this.state.STUDENTS[0]} onStudentsChange={this.onStudentsChange} />)
studentsFormElementsTemp.push(<StudentsFormElement id={studentsElementsIdArrayTemp[studentsElementsIdArrayTemp.length - 1] + 1}
key={studentsElementsIdArrayTemp[studentsElementsIdArrayTemp.length - 1] + 1}
student={this.state.STUDENTS[studentsElementsIdArrayTemp[studentsElementsIdArrayTemp.length - 1] + 1]} onStudentsChange={this.onStudentsChange} />)
There are other refactorings I can point out, but they are irrelevant to the question asked.
Other refactoring as you asked,
import React from "react";
import { InputGroup, FormControl, Button, Form } from "react-bootstrap";
class App extends React.Component {
constructor(props) {
super(props);
let identifier = 0;
this.state = {
students: [
{
identifier: identifier++,
id: "",
name: ""
}
],
identifier
};
}
addStudent = () => {
this.setState((prevState) => {
const newStudents = [...prevState.students];
newStudents.push({
identifier: prevState.identifier,
id: "",
name: ""
});
return {
identifier: prevState.identifier + 1,
students: newStudents
};
});
};
onDeleteStudent = (identifier) => {
console.log(identifier);
const filteredStudents = this.state.students.filter(
(student) => student.identifier !== identifier
);
this.setState({
students: filteredStudents
});
};
onInputChange = (event, fieldName, identifier) => {
const newStudents = [...this.state.students];
newStudents.forEach((student) => {
if (student.identifier === identifier) {
student[fieldName] = event.target.value;
}
});
this.setState(newStudents);
};
render() {
return (
<div>
<h2 style={{ textAlign: "center", display: "inline-block" }}>
Students
</h2>
<Button
id="+"
style={{ display: "inline-block" }}
variant="success"
onClick={this.addStudent}
>
+
</Button>
<Form>
{this.state.students.map((student, index) => (
<StudentsFormElement
key={student.identifier}
student={student}
onInputChange={this.onInputChange}
onDeleteStudent={this.onDeleteStudent}
/>
))}
</Form>
<p>{JSON.stringify(this.state.students)}</p>
</div>
);
}
}
class StudentsFormElement extends React.Component {
render() {
const { identifier, id, name } = this.props.student;
return (
<InputGroup className="mb-3">
<FormControl
name="id"
defaultValue={id}
placeholder="Id"
onChange={(event) => {
this.props.onInputChange(event, "id", identifier);
}}
/>
<FormControl
name="name"
defaultValue={name}
placeholder="Name"
onChange={(event) => {
this.props.onInputChange(event, "name", identifier);
}}
/>
<InputGroup.Append style={{ display: "inline-block" }}>
<Button
variant="danger"
onClick={() => {
this.props.onDeleteStudent(identifier);
}}
>
X
</Button>
</InputGroup.Append>
</InputGroup>
);
}
}
export default App;