SetState overwrites previous state even with spread operator React.JS - javascript

I'm facing an issue with setState inside my handleChange.
So here in code after clicking addList a div with input and button will appear, then when i type inside input the input state will update and after clicking Add button the div will disappear.
so my problem is next time when clicking addList the value of previous input is still there and the hideAddList handler does not set the input state to empty and also when I type in the new opened div the previous input state will overwrite. even with indexing and spread operator its still same problem.
what should I do?
export class myClass extends Component {
constructor(props) {
super(props);
this.state = {
input: [{ title: '' }]
};
this.addList = this.addList.bind(this);
this.hideAddList = this.hideAddList.bind(this);
this.titleHandleChange = this.titleHandleChange.bind(this);
}
addList() {
var x = document.getElementById("btn-list")
var y = document.getElementById("add-button")
x.style.display = 'none'
y.style.display = ''
}
hideAddList() {
var x = document.getElementById("btn-list")
var y = document.getElementById("add-button")
x.style.display = ''
y.style.display = 'none';
this.setState(prevState => ({ input: [...prevState.input, {title:''}]}))
}
HandleChange(e, index){
const { name, value } = e.target;
const list = [...this.state.input];
list[index][name] = value;
this.setState({ input: list});
}
render() {
return (
<div>
<button id="btn-list" className="btn btn-default btn-list" type="submit" onClick={this.addList}>Add Another List</button>
{this.state.input.map((x, i) => {
return (
<div key={i} id="add-button" className="add-button" style={{ display: "none" }}>
<input id="input" type="text" onChange={(e) => {this.HandleChange(e, i)}} value={x.title} name="title" className="form-control add-input" />
<button className="btn btn-default btn-list-add" onClick={this.hideAddList} type="submit">Add</button>
</div>
)})}
</div>
)
}
}
export default myClass

from the code you provided, you will always get the first "add-button" element each time you call addList, because all of your list items have the same id.
{this.state.input.map((x, i) => {
...
<div key={i} id="add-button" ...> // here! they got the same id!
so what happened here is that you are actually editing the first item all the time. you will need to add different id to your elements. and the AddList should get the last item index.
for example:
AddList()
...
{this.state.input.map((x, i) => {
return (
<div key={i} id={`add-button-${i}`} .... // here! add unique id for each item!
the whole code should be like this:
export default class myClass extends React.Component {
constructor(props) {
super(props);
this.state = {
input: [{title: ''}],
};
this.addList = this.addList.bind(this);
this.hideAddList = this.hideAddList.bind(this);
this.titleHandleChange = this.titleHandleChange.bind(this);
}
addList() {
var lastItemIndex = this.state.input.length - 1; // you need to get the last item's index to make sure that you display the latest item
var x = document.getElementById('btn-list');
var y = document.getElementById(`add-button-${lastItemIndex}`);
x.style.display = 'none';
y.style.display = '';
}
hideAddList(i) {
var x = document.getElementById('btn-list');
var y = document.getElementById(`add-button-${i}`);
x.style.display = '';
y.style.display = 'none';
this.setState(prevState => ({input: [...prevState.input, {title: ''}]}));
}
titleHandleChange(e, index) {
const {name, value} = e.target;
const list = [...this.state.input];
list[index][name] = value;
this.setState({input: list});
}
render() {
return (
<div>
<button
id="btn-list"
className="btn btn-default btn-list"
type="submit"
onClick={this.addList}>
Add Another List
</button>
{this.state.input.map((x, i) => {
return (
<div
key={i}
id={`add-button-${i}`}
className="add-button"
style={{display: 'none'}}>
<input
id="input"
type="text"
onChange={e => {
this.titleHandleChange(e, i);
}}
value={x.title}
name="title"
className="form-control add-input"
/>
<button
className="btn btn-default btn-list-add"
onClick={() => this.hideAddList(i)}
type="submit">
Add
</button>
</div>
);
})}
</div>
);
}
}
for debugging, maybe you can try to log index out in titleHandleChange, so you can find out which item that you're actually editing.
ps, you could write export default class MyClass extends ... so you don't need to write export default MyClass at the end of the file again.

The state has already been changed correctly. You can add a console log to see it:
render() {
console.log(this.state);
return (
...
The reason that it is not showing on your screen is the {display: 'none'} that you set for id 'add-button'. Your var y = document.getElementById("add-button") will just get the first element, but not the whole list of elements.
FYI it is not quite right to use document.getElementById in a react project for your case. It should be done by state. Here is a complete example:
export class MyClass extends Component {
constructor(props) {
super(props);
this.state = {
input: [{title: ''}],
showAnotherListBtn: true,
showList: false,
};
}
onClickAnotherListBtn = () => {
this.setState({
showAnotherListBtn: false,
showList: true,
});
}
addList = () => {
this.setState({
input: [...this.state.input, {title: ''}],
});
}
handleChange(e, index) {
const {name, value} = e.target;
const list = [...this.state.input];
list[index][name] = value;
this.setState({input: list});
}
render() {
console.log(this.state);
return (
<div>
{this.state.showAnotherListBtn ?
<button id="btn-list" className="btn btn-default btn-list" type="submit" onClick={this.onClickAnotherListBtn}>Add Another
List</button> : null}
{this.state.showList && this.state.input.map((x, i) => {
return (
<div key={i} id="add-button" className="add-button">
<input id="input" type="text" onChange={(e) => {
this.handleChange(e, i)
}} value={x.title} name="title" className="form-control add-input"/>
<button className="btn btn-default btn-list-add" onClick={this.addList} type="submit">Add</button>
</div>
)
})}
</div>
)
}
}
export default MyClass
As you can see from the above code, you an just use a boolean state to determine whether you should show the element or not.
Hope this helps ;)

Related

How can I make only one list item expand when a button is clicked in React (no hooks)?

I have a list of students and I want to make an expandable list view. Basically each student will have a plus & minus button and every time it's clicked a list of all student's grades will expand. However, so far when I click on one of the buttons it opens all the students' grade lists at the same time.
I can't seem to figure out how I can fix this so that when I click on a specific student it will open only that student's grades. Can anyone help me on this? I think I'm close, but I can't get there!
Here's my code:
import React from 'react';
import Average from './Average';
import './App.css';
class App extends React.Component{
state = {
students: [],
error: null,
search: null,
open: false,
}
componentDidMount(){
fetch('https://api.hatchways.io/assessment/students')
.then(res => res.json())
.then(
(data) => {
this.setState({
students: data.students,
})
},
(error) => {
this.setState({ error });
})
}
handleChange = (e) => {
this.setState({ search: e.target.value });
}
togglePanel = e => {
this.setState({ open: !this.state.open })
}
render(){
let { search, students, open } = this.state;
const display = students.filter((student) => {
if(search == null)
return student;
else if(student.firstName.toLowerCase().includes(search.toLowerCase()) ||
student.lastName.toLowerCase().includes(search.toLowerCase())){
return student;
}
}).map((student, idx) => {
return (
<div className = "student-container" key = {student.id}>
<img className = "student-pic" src = {student.pic} alt = "pictures"/>
<div className = "student-mini">
<h1>{student.firstName.toUpperCase()} {student.lastName.toUpperCase()}</h1>
<p>Email: {student.email}</p>
<p>Company: {student.company}</p>
<p>Skill: {student.skill}</p>
<Average grades = {student.grades}/>
<button onClick = {(e) => this.togglePanel(e)}>{open ? '-' : '+'}</button>
<div>
<p>{open ? student.grades.map((grade, id) => {
return (
<p key = {id}>Test {id + 1}: {grade}%</p>
)
}): null}
</p>
</div>
</div>
</div>
)
})
return (
<div className = "gray-container">
<div className = "white-container">
<form>
<input onChange = {this.handleChange} type = "text" placeholder = "Search by
name">
</input>
</form>
<div>
{display}
</div>
</div>
</div>
)
}
}
export default App;
Each of your expandable panels uses the same single open state to determine whether it should be expanded or not, that is why they all open at once. The easiest solution here is for each panel to have its own state.
Ideally, you want to extract the JSX from your map's return statement to separate component presenting a single student's panel, let's say StudentPanel, pass the student object as prop and move the open state and the toggle function to that component as well.
UPDATE: You'd have something like this:
class StudentPanel extends React.Component {
state = {
open: false
}
togglePanel = e => {
this.setState(prevState => ({ open: !prevState.open }))
}
render() {
const { student } = this.props;
return (
<div className = "student-container" key = {student.id}>
// (...) rest of JSX
</div>
);
}
}
In render of your App you'd have:
(...).map(student => <StudentPanel student={student} />)
and the open property and togglePanel can be removed from App component completely.

How to change the button colour when it is clicked

I have this set of buttons from A to Z when I click on the particular alphabet the brands related to that letter will pop up. I have tried the below code but it is applying for all the buttons when I click only one button. How do I solve it
let brandList = 'ABCEDEFGHIJKLMNOPQRSTUVWXYZ'.split("");
constructor(props) {
super(props)
this.state = {
bgColor: ""
}
}
getBrandSortData(){
return(
<div className="BrandPageList_AlphabetContainer">
<button value="all" className="BrandPageList_AllButton" onClick={this.handleClick}>All</button>
{brandList.map((item,index) =>
{
let disbaled = !this.isBrandCharacterAvailable(item)
return(
<button
disabled= {disbaled}
value={item}
key={index}
block="BrandPageList_AlphabetButtons"
mods = {{enabled : !disbaled}}
onClick={this.handleClick}
style={{backgroundColor: this.state.bgColor}}
>
{item}
</button>
)}
)}
</div>
)
}
handleClick = event =>{
const brandValues = event.target.value
if(brandValues === "all"){
console.log()
return this.setAllBrands()
}
else{
let brandSortDataByCharacter = this.state.brandSortData[brandValues]
this.setState({
allBrands:
{
[brandValues]: brandSortDataByCharacter
},
bgColor: "red"
})
}
}
This is the imageThanks in advance
Instead of using the event object to determine what letter you clicked on, you can pass the letter to the click handler when you render it.
Here's an example of how you can achieve this. I'm using a functional component with hooks, since this is the recommended way, but you can do the same in a class component:
const Alphabet = () => {
const [active, setActive] = useState();
return (
<div className='alphabet'>
{'ABCEDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map(char => (
<div
className={`letter ${char === active ? 'active' : ''}`}
onClick={() => setActive(char)}>{char}</div>
))}
</div>
);
};
And here's a live example

How to automatically generate Input fields related to list of items React.JS

I'm having a problem in dynamically generating input fields in react. So I have a keys state and I need to have an input field for each key and also I have tried this link: How to implement a dynamic form with controlled components in ReactJS?
but the problem is that the my values state is empty so it will render nothing regarding keys and when I did it with this.ModifyList() it shows input fields regarding each key but it does not have onChange mehtod. the onChange method causes the error when using the this.createUI() .
Also in the end I would like to submit the values of input fields.
Is there any suggestion on how to solve this problem?
my Code Below:
export class FileUploadComponent extends Component {
constructor(props) {
super(props);
this.state = {
//Keys: [],
//values: [],
modify: { Keys: ['key1' , 'key2' , 'key3'], values: [] }
}
this.handleSubmit = this.handleSubmit.bind(this);
}
createUI() {
const { modify } = this.state;
const keys = modify.Keys
const values = modify.values
const val = keys.map(function (item, i) {
values.map(function (el, i) {
return <div key={i}>
<label>{item}</label>
<input type="text" onChange={this.handleChange.bind(this, i)} />
</div>
})
});
return val;
}
handleChange(event, i) {
const {modify} = this.state;
let values = [...modify.values];
values[i] = event.target.value;
this.setState({ values: values });
}
handleSubmit(event) {
alert('A name was submitted: ' + this.state.values.join(', '));
event.preventDefault();
}
ModifyList() {
const { modify } = this.state;
const keys = modify.Keys
const val = keys.map(function (item, i) {
return <div>
<label>{item}</label>
<input type="text" />
</div>
});
return val;
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
{/*this.ModifyList()*/}
{this.createUI()}
<input type="submit" className="btn btn-primary" value="Search !" />
<input type="submit" className="btn btn-primary" value="Edit !" />
</form>
</div>
)
}
}
export default FileUploadComponent
You have some some scope issue. One of the main difference between a fat-arrow function and a function declared with the function keyword is that the latter has its own scope, meaning that if you call this inside of it, you are referencing its scope.
In your createUI function, switch your functions to fat-arrow functions and you are all set. Just remember to bind your handle change function in your constructor.
export class FileUploadComponent extends Component {
constructor(props) {
super(props);
this.state = {
//Keys: [],
//values: [],
modify: { Keys: ["key1", "key2", "key3"], values: [""] }
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
createUI() {
const { modify } = this.state;
const keys = modify.Keys;
const values = modify.values;
const val = keys.map((item, i) => {
return values.map((el, i) => {
return (
<div key={i}>
<label>{item}</label>
<input
type="text"
onChange={(event) => this.handleChange(event, i)}
/>
</div>
);
});
});
return val;
}
handleChange(event, i) {
const { modify } = this.state;
let values = [...modify.values];
values[i] = event.target.value;
this.setState({ values: values });
}
handleSubmit(event) {
alert("A name was submitted: " + this.state.values.join(", "));
event.preventDefault();
}
ModifyList() {
const { modify } = this.state;
const keys = modify.Keys;
const val = keys.map(function (item, i) {
return (
<div>
<label>{item}</label>
<input type="text" />
</div>
);
});
return val;
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
{/*this.ModifyList()*/}
{this.createUI()}
<input type="submit" className="btn btn-primary" value="Search !" />
<input type="submit" className="btn btn-primary" value="Edit !" />
</form>
</div>
);
}
}

how to separate multiple input from the same div and get it in the parent component accordingly

I have a input (Inputu) function like so :
cexport default ({ onChange, value1, value2, value3, name }: any) => {
return (
<div name={name}>
<form >
<div>
<label> </label>
<input value={value1} onChange={onChange} />
</div>
<div>
<label></label>
<select onChange={onChange} value = {value2}>
<option>text</option>
</select>
</div>
<div >
<label></label>
<select value={value3} onChange={onChange}>
<option>text</option>
</select>
</div>
</form>
</div>
this is a dynamic function that added in the app component as the user pressing a button like so:
class App extends Component {
constructor(props: any) {
super(props);
this.state = {
clikctime: 0,
q1: []
};
}
onChange = ({ currentTarget }) => {
const value1= currentTarget.value.value1;
const value2 = currentTarget.value.value2;
const value3 = currentTarget.value.value3;
const index = this.state.clikctime;
let q1 = [...this.state.q1];
q3[index - 1] = [value1,value2 ,value3];
this.setState({ q1 });
};
renderInputs = ({ value1, value2, value3 }, idx) => {
const name = this.state.clikctime;
return (
<Inputu key={idx} value1={value1} value2={value2} value3={value3} name={name} onChange=
{this.onChangeu} />
);
};
addInputs = () => {
const newclick = this.state.clikctime;
const nw = newclick + 1;
this.setState({ clikctime: nw });
this.setState({ q1: this.state.q1.concat(nw) });
};
render() {
return (
<div>
<button
onClick={this.addInputs}>
click on me to add question !
</button>
</div>
<div>
{this.state.q1.map(this.renderInputs)}
</div>
);
}
}
I couldn't manage to separate the value, also how can I add the state as list\dic so the input would be associated with name\id (regardless of how many times the user klicked the button and add input)
In the end I would like to get somting like that:
q1: {name1:[value1 ,value2, value3], name2:[value1 ,value2, value3]....}
Thanks for trying to help

ReactJs Todo List push state

I have written the start of a todo list in reactjs and was looking to improve the functionality of how the todos are added to the state. Currently I am concat(ting) the value in put to an array which is in the state, then splicing off the selected li element. It seems to be a bit buggy when you add the first todo. Should i be using reacts immutability helpers to acheive this? Seems overkill to add another thing that can be acheived in plain js.
//Input component
const Input = props => {
return (
<div className="form-group">
<input
className="form-control"
value={props.value}
onChange={props.update}
type="text"
/>
<button className="btn btn-default" onClick={props.handleClick}>
Add Todo
</button>
</div>
);
};
//display list of todos
const Displaytodo = (props) => {
const todolist = props.todo;
const listItems = todolist.map((todo, index) =>
<li
className={
props.highlight ? 'list-unstyled todoItem highlight' : 'list-unstyled todoItem '
}
key={index}>
{todo}
<div
onClick={props.removeTodo.bind(this, index)}
className="removeTodo">
<i className="fa fa-trash" />
</div>
<div onClick={props.changeHighlight.bind(this,index)} className="checkTodo">
<i className="fa fa-check-circle" onClick={props.highlight} />
</div>
</li>
);
return <ul className="todos">{listItems}</ul>;
};
//controlled state component
class Layout extends React.Component {
constructor() {
super();
this.state = { text: "Hello", todo: [], highlight: false };
}
update(e) {
this.setState({ text: e.target.value });
}
handleClick() {
const text = this.state.text;
if (text.length > 0) {
this.setState(
{ todo: this.state.todo.concat(text), text: "", highlight: false },
function() {
console.log(this.state.todo);
}
);
} else {
alert("please enter something");
}
}
removeTodo(e) {
this.state.todo.splice(e, 1);
this.setState({ todo: this.state.todo });
}
changeHighlight(index, e) {
const highlight = this.state.highlight;
this.setState(prevState => ({
highlight: !prevState.highlight
}));
}
render() {
return (
<div className="container">
<div className="row">
<div className="col-md-4 col-md-offset-4">
<div className="wrapper">
<h1>Todo List</h1>
<Input
value={this.state.text}
update={this.update.bind(this)}
handleClick={this.handleClick.bind(this)}
/>
<Displaytodo
removeTodo={this.removeTodo.bind(this)}
todo={this.state.todo}
changeHighlight={this.changeHighlight.bind(this)}
highlight={this.state.highlight}
/>
</div>
</div>
</div>
</div>
);
}
}
const app = document.getElementById("app");
ReactDOM.render(<Layout />, app);
https://codepen.io/mhal12/pen/MomWVg
Also when the user clicks the green tick, it will highlight the row by toggling class 'highlight' off and on, but in console it giving an error. which links to
https://facebook.github.io/react/docs/error-decoder.html?invariant=94&args[]=onClick&args[]=boolean
Simply remove the onClick on <i className="fa fa-check-circle" onClick={props.highlight} />.
As for the highlighting on each todo, it's a bit more complex. You have to have an id on each todo, and then pass the id to the changeHighlight function. You have to remove highlight from global state, and assign a highlight boolean on each todo. Then you have to display todos accordingly.
Same stuff for the removeTodo function, you pass in an id to remove it in the parent component.
Here's the full code :
const Input = props => {
return (
<div className="form-group">
<input
className="form-control"
value={props.value}
onChange={props.update}
type="text"
/>
<button className="btn btn-default" onClick={props.handleClick}>
Add Todo
</button>
</div>
);
};
const Displaytodo = (props) => {
const changeHighlight = function(id) {
props.changeHighlight(id);
}
const removeTodo = function(id) {
props.removeTodo(id);
}
const todolist = props.todo;
const listItems = todolist.map((todo, index) =>
<li
className={
todo.highlight ? 'list-unstyled todoItem highlight' : 'list-unstyled todoItem '
}
key={todo.id}>
{todo.text}
<div
onClick={removeTodo.bind(event, todo.id)}
className="removeTodo">
<i className="fa fa-trash" />
</div>
<div onClick={changeHighlight.bind(event, todo.id)} className="checkTodo">
<i className="fa fa-check-circle" />
</div>
</li>
);
return <ul className="todos">{listItems}</ul>;
};
class Layout extends React.Component {
constructor() {
super();
this.state = {text: "Hello", todo: []};
}
update(e) {
this.setState({ text: e.target.value });
}
handleClick() {
const text = this.state.text;
if (text.length > 0) {
this.setState(
{ todo: this.state.todo.concat({
id: this.state.todo.length + 1,
text: this.state.text,
highlight: false
}), text: ""},
function() {
console.log(this.state.todo);
}
);
} else {
alert("Please enter something");
}
}
removeTodo(id) {
let todos = this.state.todo;
for (let i = 0; i < todos.length; i++) {
let todo = todos[i];
if (todo.id == id) {
todos.splice(i, 1);
}
}
this.setState({ todo: todos });
}
changeHighlight(id) {
let todos = this.state.todo;
for (let i = 0; i < todos.length; i++) {
let todo = todos[i];
if (todo.id == id) {
todos[i].highlight = !todos[i].highlight;
}
}
this.setState({
todo : todos
});
}
render() {
return (
<div className="container">
<div className="row">
<div className="col-md-4 col-md-offset-4">
<div className="wrapper">
<h1>Todo List</h1>
<Input
value={this.state.text}
update={this.update.bind(this)}
handleClick={this.handleClick.bind(this)}
/>
<Displaytodo
removeTodo={this.removeTodo.bind(this)}
todo={this.state.todo}
changeHighlight={this.changeHighlight.bind(this)}
/>
</div>
</div>
</div>
</div>
);
}
}
const app = document.getElementById("app");
ReactDOM.render(<Layout />, app);

Categories

Resources