react is editing all array objects instead of just one array object - reactjs

Currently a user can enter first, lastname, and employee id. When it submits it renders an iteration of employees with the first, last, and employee id.
The problem is when you click edit, it edits the data but it will edit all of the fields for every object in the employees iteration.
How can I fix the code so that it will edit just that particular object within that iteration.
Home component
........
class Login extends Component {
constructor(props) {
super(props);
this.state = {
activeTab: '3',
firstName: '',
lastName: '',
employeeID: '',
employees: [],
Edit: false,
updatedFirst: '',
updatedLast:'',
updatedEmployeeID: ''
};
}
toggle = (tab) => {
if(this.state.activeTab !== tab){
this.setState({
activeTab: tab
})
}
}
onSubmit = (e) => {
e.preventDefault();
const {firstName, lastName, employeeID} = this.state
const ourForm ={
firstName: firstName,
lastName: lastName,
employeeID: employeeID,
// we need an id to so that it can be edited properly
}
this.setState({
employees: [...this.state.employees,ourForm]
}, () => {
console.log(this.state.employees)
})
}
onChange = (e) => {
e.preventDefault()
// e.preventDefault();
this.setState({
[e.target.name]: e.target.value,
});
}
updatedChange = (e) => {
e.preventDefault()
// e.preventDefault();
this.setState({
[e.target.name]: e.target.value,
});
}
onEdit = (e) => {
e.preventDefault();
this.setState({
Edit: !this.state.Edit
})
}
onReset = (e) => {
e.preventDefault();
this.setState({
firstName: '',
lastName: '',
employeeID: ''
})
}
render(){
return (
......
<MyForm
onSubmit={this.onSubmit}
onChange={this.onChange}
onReset={this.onReset}
firstName={this.state.firstName}
lastName={this.state.lastName}
employeeID={this.state.employeeID}
/>
</Col>
</Row>
</TabPane>
</TabContent>
</Container>
<List
employees={this.state.employees}
Edit={this.state.Edit}
onEdit ={this.onEdit}
onChange={this.onChange}
updatedEmployeeID={this.state.updatedEmployeeID}
updatedFirst={this.state.updatedFirst}
updatedLast={this.state.updatedLast}
/>
</div>
);
}
}
export default Login;
Form.js
import React, {Component} from 'react';
import { Col, Form, FormGroup, Label, Input, Button } from 'reactstrap';
const MyForm = (props) => {
return(
<Form style={{ margin: '30px 0px'}}>
<FormGroup row>
<Label for="firstName" sm={2} size="sm">First Name:</Label>
<Col sm={10}>
<Input type="text" onChange={props.onChange} value={props.firstName} name="firstName" id="exampleEmail" placeholder="Enter First Name"/>
</Col>
</FormGroup>
<FormGroup row>
<Label for="lastName" sm={2} size="sm">Last Name:</Label>
<Col sm={10}>
<Input type="text" onChange={props.onChange} value={props.lastName} name="lastName" id="exampleEmail2" placeholder="Enter Last Name" />
</Col>
</FormGroup>
<FormGroup row>
<Label for="Employee ID" sm={2} size="sm">Employee ID:</Label>
<Col sm={5}>
<Input type="text" onChange={props.onChange} value={props.employeeID} name="employeeID" id="exampleEmail2" placeholder="Enter Employee ID" />
</Col>
</FormGroup>
<FormGroup row>
<Col sm={12}>
<div className="float-right">
<Button onClick={props.onSubmit} size="lg" style={{ margin: '0px 5px'}} color="secondary">Add</Button>
<Button onClick={props.onReset} size="lg" style={{ margin: '0px 5px'}}color="warning">Reset</Button>
</div>
</Col>
</FormGroup>
<hr></hr>
<FormGroup row>
<Col sm={4}>
<Input type="text" name="search" id="exampleEmail2" placeholder="Search" />
</Col>
<Col sm={8}>
<Label for="sort" sm={2} size="sm">Sort:</Label>
<Button onClick={props.onSubmit} size="lg" style={{ margin: '0px 5px'}} color="secondary">First Name</Button>
<Button onClick={props.onSubmit} size="lg" style={{ margin: '0px 5px'}} color="secondary">Last Name</Button>
<Button onClick={props.onSubmit} size="lg" style={{ margin: '0px 5px'}} color="secondary">ID</Button>
</Col>
</FormGroup>
</Form>
)
}
export default MyForm;
List component
import React, {Component, Fragment} from 'react';
import { Col, Form, FormGroup, Label, Input, Button } from 'reactstrap';
const List = (props) => {
return(
<Fragment>
{props.employees.map( (item, i) => (
<div style={{ margin: '40px 0px'}} key={i}>
<hr style={{ border:'1px dashed #000'}}></hr>
<div className="float-right">
<Button onClick={props.onEdit} size="lg" style={{ margin: '0px 5px'}} color="secondary">{props.Edit ? 'Save': 'Edit'}</Button>
<Button size="lg" style={{ margin: '0px 5px'}}color="secondary">Delete</Button>
</div>
<FormGroup row>
<Col sm={5}>
{props.Edit ? (
<Input type="text" onChange={props.onChange} value={ props.updatedFirst ? props.updatedFirst : item.firstName } name="updatedFirst" placeholder="Enter First Name"/>
):(
<div>
{props.updatedFirst ? props.updatedFirst : item.firstName }
</div>
)}
</Col>
</FormGroup>
<FormGroup row>
<Col sm={5}>
{props.Edit ? (
<Input type="text" onChange={props.onChange} value={ props.updatedEmployeeID ? props.updatedEmployeeID : item.employeeID} name="updatedEmployeeID" placeholder="Enter EmployeeID"/>
):(
<div>
{props.updatedEmployeeID ? props.updatedEmployeeID : item.employeeID}
</div>
)}
</Col>
</FormGroup>
<FormGroup row>
<Col sm={5}>
{props.Edit ? (
<Input type="text" onChange={props.onChange} value={ props.updatedLast ? props.updatedLast: item.lastName} name="updatedLast" placeholder="Enter Last Name"/>
):(
<div>
{props.updatedLast ? props.updatedLast : item.lastName}
</div>
)}
</Col>
</FormGroup>
</div>
))}
</Fragment>
)
}
export default List;

The following example shows how to pass a handler and set the state accordingly.
For good measure I separated the logic and the presentation, the presentational components are pure components using React.memo.
//A container should only contain the logic
class EmployeesContainer extends React.Component {
state = {
employees: [{ name: '' }, { name: '' }, { name: '' }],
};
//define what needs to happen if you click edit on an
// employee
onEdit = index => {
//edit will be called with the index of the employee
// the Employees component owns the list of employees
// so it will have to make changes to it
this.setState({
employees: this.state.employees.map((employee, i) =>
i === index ? { ...employee, edit: true } : employee
),
});
};
//Same idea as onEdit, index needs to be passed to indicate
// what employee needs to be changed
onChange = (index, e) => {
this.setState({
employees: this.state.employees.map((employee, i) =>
i === index
? { ...employee, name: e.target.value }
: employee
),
});
};
render() {
return (
<Employees
employees={this.state.employees}
onEdit={this.onEdit}
onChange={this.onChange}
/>
);
}
}
//The Employees presentational component, contains the jsx
// you can make it a pure component by using React.memo
const Employees = React.memo(
({ employees, onEdit, onChange }) => (
<div>
{employees.map((employee, index) => (
<EmployeeContainer
key={index}
index={index}
employee={employee}
onEdit={onEdit}
onChange={onChange}
/>
))}
</div>
)
);
//Make this a container as well because it does more
// than only produce jsx
class EmployeeContainer extends React.Component {
state = {};
//create onChange and onEdit only when index changes
// this will prevent unnecessary renders
static getDerivedStateFromProps(props, state) {
const { index, onChange, onEdit } = props;
if (state.index !== index) {
return {
index,
onChange: e => onChange(index, e),
onEdit: () => onEdit(index),
};
}
return null;
}
render() {
const { employee } = this.props;
const { onChange, onEdit } = this.state;
return (
<Employee
employee={employee}
onChange={onChange}
onEdit={onEdit}
/>
);
}
}
//presentational component, is also pure component
const Employee = React.memo(
({ employee, onChange, onEdit }) => (
<div>
{employee.edit ? (
<input
type="text"
value={employee.name}
onChange={onChange}
/>
) : (
<button onClick={onEdit}>edit</button>
)}
</div>
)
);

I don’t think onSubmit is updating employees correctly. You shouldn’t use this.state inside setState.
this.state inside setState ReactJS
Try this..
this.setState(prevState => ({
employees: [...prevState.employees, ourForm]
}, () => {
console.log(this.state.employees)
}))

Related

How do I resolve this error for a react-bootstrap-typeahead

I have a child component called ProductCard that takes props from the parent called CreateNewSales. The onChange function takes two argument and is defined in the parent. If I call that function in the ProductCard, I keep getting this error for the typeahead though every other input except the quantity input seems to be working just fine. Here are the code:
This is the ProductCard:
import React, { useState, Fragment } from 'react';
import { FormGroup, Label, Input, Col} from 'reactstrap';
import { Typeahead } from 'react-bootstrap-typeahead';
import PropTypes from 'prop-types';
import {PlusSquare, MinusSquare} from 'react-feather'
const plusSquare = <PlusSquare/>
const minusSquare = <MinusSquare/>
const ProductCard = (props) => {
console.log(props)
const ProductOptions = [{id:1,name: "Clothes"}, {id:2,name:"Services"}, {id:3,name:"Shows"}, {id:4,name: "Peace"}] //dummy date //res_data.payload.data.product
// const [singleSelections, setSingleSelections] = useState([]);
// from quantityIncrement.js
const [count, setCount] = useState(1)
const decrement = () => {
if(count === 1){
setCount(count)
} else{
setCount(count-1)
}
};
const increment = () => {
setCount(count+1)
};
return(
<Fragment>
<FormGroup>
<Label>Product Name</Label>
<Typeahead
id="basic-typeahead"
labelKey="name"
onChange={(e) => props.handleChange(e, props.index)}
// onInputChange={props.setProductName}
options={ProductOptions}
name={ProductOptions}
selected={props.value.productName}
style={{backgroundColor:"#d5deee"}}
value={props.value.productName}
/>
</FormGroup>
<FormGroup>
<div className="form-row">
<Col>
<Label for="quantity">Quantity</Label>
<div style={{display: "flex"}}>
<Input value={count} id="quantity" name="quantity" onChange={e=> setCount(e.target.value)} style={{backgroundColor: "#d5deee"}} />
<div style={{display:"flex", marginTop:"5px"}}><span><i style={{ width: 15, fontSize: 10, padding: 11, color: '#848b97' }} onClick={increment}>{plusSquare}</i></span><span><i style={{ width: 15, fontSize: 12, color: '#848b97' }} onClick={decrement}>{minusSquare}</i></span></div>
</div>
</Col>
<Col>
<Label for="discount">Discount</Label>
<Input type="text" id="discount" onChange = {(e) => props.handleChange(e, props.index)} value={props.value.discount} name="discount" style={{backgroundColor:"#d5deee"}} />
</Col>
</div>
</FormGroup>
<FormGroup>
<div className="form-row">
<Col>
<Label for="price">Price</Label>
<Input type="text" id="price" onChange={(e) => props.handleChange(e, props.index)} value={props.value.price} name="price" style={{backgroundColor:"#d5deee"}} />
</Col>
<Col>
<Label for="amountPaid">Amount Paid</Label>
<Input type="text" id="amountPaid" onChange={(e) => props.handleChange(e, props.index)} value={props.value.amountPaid} name="amountPaid" style={{backgroundColor:"#d5deee"}} />
</Col>
</div>
</FormGroup>
</Fragment>
)
}
ProductCard.propTypes = {
handleChange: PropTypes.func.isRequired,
value: PropTypes.object.isRequired,
// onClickQuantity: PropTypes.func.isRequired
}
export default ProductCard
This is the CreateNewSale:
import React, { Fragment, useState } from 'react';
import {Form, FormGroup, Label, Input, Card, Col, Row, CardBody, Button, ButtonGroup, Table} from 'reactstrap';
import {toast} from 'react-toastify';
import { Typeahead } from 'react-bootstrap-typeahead';
import { withRouter} from 'react-router';
// import {useHistory} from 'react-router-dom';
import {connect} from 'react-redux';
import {validateCreateNewSaleForm, responseErrorParser} from "../../components/authentication/validator"
// productCard.js
import ProductCard from '../../components/sales/ProductCard'
import {Trash2} from 'react-feather'
const trash2 = <Trash2/>
const CreateNewSale = (props) => {
const [, setIsCreatingNewSale] = useState(false)
// const [singleSelections, setSingleSelections] = useState([]);
// from the productCard.js
const [newProductValues, setNewProductValues] = useState([{
productName: [],
discount: "",
price: "",
amountPaid: "",
quantity: "1",
}]);
// const [newProductName, setNewProductName] = useState([{
// newProductNames: [],
// newCustomerName: []
// }])
// const handleInputChange = (event) => {
// setNewProductName({
// ...newProductName,
// [event.target.name]: event.target.value
// });
// }
const [customerName, setCustomerName] = useState([])
const [date, setDate] = useState('')
const [dueDate, setDueDate] = useState('')
const [vat, setVat] = useState('')
// const [visible, setVisible] = useState(false)
const handleChange = (event, index) => {
console.log( event )
const values = [...newProductValues];
values[index][event.target.name] = event.target.value
console.log('=======================>', values)
setNewProductValues(values);
// setNewProductValues({
// ...newProductValues,
// [event.target.name]: event.target.value
// });
}
const handleAddFields = (e) => {
setNewProductValues([...newProductValues, {discount:"", price: "",
amountPaid: "",
quantity: "1",
productName:[]
}])
}
const handleRemoveFields = (index) => {
const values = [...newProductValues];
values.splice(index, 1);
setNewProductValues(values);
}
const customerOptions = [{id: 1, name: "Olalekan"}, {id: 2, name:"Michael"}, {id: 3,name:"Emeka"}, {id:4,name: "Glory"}] //dummy data //res_data.payload.data.customer
const fields = {
customer_name: { default: '', message: 'Please enter an already created customer name' },
product_name: { default: '', message: 'Please enter an already created customer name' },
quantity: { default: '', message: 'Please select a quantity' },
discount: { default: '', message: 'Please enter the discount given' },
price: { default: '', message: 'Please select the price given' },
amount_paid: { default: '', message: 'Please enter the amount paid by the customer' },
date: { default: '', message: 'Please enter date' },
due_date: { default: '', message: 'Please enter due date given' },
vat: { default: '', message: 'Please enter the vat' },
}
const handleCreateNewSale = async (e) => {
e.preventDefault()
setIsCreatingNewSale(true);
const responsePayload = {
customer_name: newProductValues.newCustomerName, //customerName
product_name: newProductValues.newproductNames,
quantity: newProductValues.quantity,
discount: newProductValues.discount,
price: newProductValues.price,
amount_paid: newProductValues.amountPaid,
date: date,
due_date: dueDate,
vat: vat
}
const errors = validateCreateNewSaleForm(responsePayload, fields)
if (errors.isErrors) {
setIsCreatingNewSale(false)
setTimeout(() => {
errors.errors.forEach(e => toast.error(e.message))
}, 400);
} else {
const response = await props.CreateNewSale(responsePayload)
if (response.status) {
const newSale = response.payload.data.id
localStorage.setItem('__grm__act__biz__', newSale.toString())
// props.history.push(`/business/${newSale}.toString/sales/invoice`, {previousLocation: props.location.pathname})
} else {
setIsCreatingNewSale(false)
const payload = response.payload
const errs = responseErrorParser(payload.data)
setTimeout(() => {
errs.forEach(e => toast.error(e.message))
}, 400);
}
}
}
return(
<Fragment>
<div style={{display:"flex", fontFamily:"'Poppins', sans-serif"}}>
<div className="col-lg-10" style={{margin: "0 auto", maxWidth:"500px", width:"100%"}}>
<Form>
<Card>
<CardBody>
<FormGroup>
<Label>Customer Name</Label>
<Typeahead
id="basic-typeahead"
labelKey="name"
onChange={setCustomerName}
options={customerOptions}
selected={customerName}
value={customerName}
name="customerName"
style={{backgroundColor:"#d5deee"}}
/>
</FormGroup>
</CardBody>
</Card>
{ newProductValues.map((newProductValue, index) => (
<div key={index}>
<Card >
<CardBody>
<Col style={{textAlign: "right"}}>
<i onClick={() => handleRemoveFields()} >{trash2}</i>
</Col>
<ProductCard index={index} handleChange={handleChange} value={newProductValue} />
</CardBody>
</Card>
</div>
))}
<Row>
<Col>
<p onClick={() => handleAddFields()} style={{marginLeft:"20px"}}> <span style={{fontSize:"18px"}}>+</span> Add another product</p>
</Col>
</Row>
<Row>
<Col>
<p onClick={() => handleAddFields()} style={{marginLeft:"20px"}}> <span style={{fontSize:"18px"}}>+</span> Add another product</p>
</Col>
</Row>
<Card>
<CardBody>
<FormGroup>
<div className="form-row">
<Col>
<Label for="date">Date</Label>
<Input className="form-control digits" type="date" defaultValue="2018-01-01" value={date} onChange={e => setDate(e.target.value)} id="date" name="date" style={{backgroundColor:"#d5deee"}} />
</Col>
<Col>
<Label for="dueDate">Due Date</Label>
<Input className="form-control digits" type="date" defaultValue="2018-01-01" value={dueDate} onChange={e => setDueDate(e.target.value)} id="dueDate" name="dueDate" style={{backgroundColor:"#d5deee"}} />
</Col>
</div>
</FormGroup>
<FormGroup>
<div className="form-row">
<Col>
<Label for="vat">VAT %</Label>
<Input type="text" id="vat" value={vat} onChange={e => setVat(e.target.value)} style={{backgroundColor:"#d5deee"}} />
</Col>
</div>
</FormGroup>
<div style={{margin:"0 auto", textAlign:"center"}}>
<p style={{fontSize:"12px"}}>Only click cleared if this sales have been paid in full</p>
<Row>
<Col>
<ButtonGroup>
<Button outline color="primary" type="button">Cleared</Button>
<Button outline color="primary" type="button">Not Cleared</Button>
</ButtonGroup>
</Col>
</Row>
</div>
<Row className="m-t-50">
<Col lg={`6`}>
<Button outline color="primary" size="lg" style={{maxWidth:"200px", width:"100%"}}>SAVE</Button>
</Col>
<Col lg={`6`}>
<Button color="primary" size="lg" onClick={e => handleCreateNewSale(e)} style={{maxWidth:"200px", width:"100%"}}>CREATE</Button>
</Col>
</Row>
</CardBody>
</Card>
</Form>
</div>
<div className="col-lg-2" style={{backgroundColor:"#eaf6fd", position:"fixed", right:0, height:"100%",}}>
<Card className="m-t-50">
<CardBody>
<div>You have added <span>0</span> products</div>
</CardBody>
</Card>
<div className="table-responsive">
<Table borderless>
<tbody>
<tr>
<td className="bd-t-none">Sub Total</td>
<td>000 000 000</td>
</tr>
<tr style={{fontWeight:"bold"}}>
<td className="bd-t-none">Total</td>
<td>000 000 000</td>
</tr>
</tbody>
</Table>
</div>
</div>
</div>
</Fragment>
)
}
const mapStateToProps = (state) => ({
requestingCreateNewSale: state.isRequestingCreateNewSale,
});
const actions = {
CreateNewSale: CreateNewSale
};
export default connect(mapStateToProps, actions)(withRouter(CreateNewSale))
And this is the error I keep getting everytime I want to change the value for the productName typeahead:
TypeError: Cannot read property 'name' of undefined
handleChange
src/pages/sales/CreateNewSales.js:54
51 | const handleChange = (event, index) => {
52 | console.log( event )
53 | const values = [...newProductValues];
> 54 | values[index][event.target.name] = event.target.value
| ^ 55 | console.log('=======================>', values)
56 | setNewProductValues(values);
57 | // setNewProductValues({
View compiled
onChange
src/components/sales/ProductCard.jsx:40
37 | <Typeahead
38 | id="basic-typeahead"
39 | labelKey="name"
> 40 | onChange={(e) => props.handleChange(e, props.index)}
| ^ 41 | // onInputChange={props.setProductName}
42 | options={ProductOptions}
43 | name={ProductOptions}

React useState hook does not shows correct state of array

I have two functional components and from parent component I am creating set of controls dynamically. Based on each item created I want to delete them but every time last one is getting deleted. For example three rows created when I delete second or first on,last one was getting deleted.
Education.jsx
function Education(props) {
const blankEdu = { id: 0, name: "", percentage: "", year: "" };
const [eduState, setEduState] = useState([{ ...blankEdu }]);
const addEducation = () => {
setEduState([...eduState, { ...blankEdu }]);
};
function handleRemove(index) {
console.log(index);
if (eduState.length != 1) {
const updatedEdu = [...eduState];
updatedEdu.splice(index, 1);
setEduState([...updatedEdu]);
}
}
const handleEducationChange = (index, e, c) => {
const updatedEdu = [...eduState];
updatedEdu[index][c] = e.target.value;
updatedEdu[index]["id"] = index;
setEduState(updatedEdu);
};
return (
<div>
<div className="shadow p-3 mb-5 bg-white rounded">
Final Step: Education
</div>
{eduState.map((val, idx) => (
<div
key={idx}
>
<EducationInput
key={`edu-${idx}`}
idx={idx}
handleEducationChange={handleEducationChange}
/>
{eduState.length > 1 ? (
<Button variant="danger" onClick={() => handleRemove(idx)}>
Remove Course
</Button>
) : null}
</div>
))}
<Button variant="outline-info" onClick={addEducation}>
Add New Degree
</Button>
</div>
);
}
export default Education;
EducationInput.jsx
const EducationInput = ({ idx, handleEducationChange }) => {
return (
<div key={`edu-${idx}`} id={`edu-${idx}`}>
<span className="border border-success">
<Form>
<Form.Group as={Row}>
<Form.Label column sm={3}>
{`Course #${idx + 1}`}:
</Form.Label>
<Col sm={5}>
<input
type="text"
onChange={e => handleEducationChange(idx, e, "name")}
/>
</Col>
</Form.Group>
<Form.Group as={Row}>
<Form.Label column sm={3}>
Passing Year:
</Form.Label>
<Col sm={5}>
<input
type="text"
onChange={e => handleEducationChange(idx, e, "year")}
/>
</Col>
</Form.Group>
</Form>
</span>
</div>
);
};
export default EducationInput;
I checked and verified value of updatedEdu by printing on console. It is giving correct output on console but setEduState function does not updating properly on UI, don't know why.
You are depending the index of the item, but indexes are changing when you add or remove elements, so they are not reliable.
You need to generate an automatic unique id when creating a new education.
For example uuid package is popular for this task.
I refactored your code a little bit to make it work:
Education:
import React, { useState } from "react";
import EducationInput from "./EducationInput";
import Button from "react-bootstrap/Button";
import uuidv4 from "uuid/v4";
function Education(props) {
const blankEdu = { id: "", name: "", percentage: "", year: "" };
const [eduState, setEduState] = useState([{ ...blankEdu }]);
const addEducation = () => {
setEduState([...eduState, { ...blankEdu, id: uuidv4() }]);
};
function handleRemove(id) {
console.log(id);
if (eduState.length > 1) {
const updatedEdus = eduState.filter(edu => edu.id !== id);
setEduState(updatedEdus);
}
}
const handleEducationChange = (id, field, value) => {
console.log(field, value);
let updatedEducations = eduState.map(edu => {
if (edu.id === id) return edu;
edu[field] = value;
return edu;
});
setEduState(updatedEducations);
};
return (
<div>
<div className="shadow p-3 mb-5 bg-white rounded">
Final Step: Education
</div>
{eduState.map(val => (
<div key={val.id}>
<EducationInput
key={`edu-${val.id}`}
idx={val.id}
handleEducationChange={handleEducationChange}
/>
{eduState.length > 1 ? (
<Button variant="danger" onClick={() => handleRemove(val.id)}>
Remove Course
</Button>
) : null}
</div>
))}
<Button variant="outline-info" onClick={addEducation}>
Add New Degree
</Button>
<br />
<br />
Educations in json:{JSON.stringify(eduState)}
</div>
);
}
export default Education;
EducationInput
import React from "react";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Row from "react-bootstrap/Row";
const EducationInput = ({ idx, handleEducationChange }) => {
return (
<div key={`edu-${idx}`} id={`edu-${idx}`}>
<span className="border border-success">
<Form>
<Form.Group as={Row}>
<Form.Label column sm={3}>
{`Course #${idx + 1}`}:
</Form.Label>
<Col sm={5}>
<input
type="text"
name="name"
onChange={e =>
handleEducationChange(idx, e.target.name, e.target.value)
}
/>
</Col>
</Form.Group>
<Form.Group as={Row}>
<Form.Label column sm={3}>
Passing Year:
</Form.Label>
<Col sm={5}>
<input
type="text"
name="year"
onChange={e =>
handleEducationChange(idx, e.target.name, e.target.value)
}
/>
</Col>
</Form.Group>
</Form>
</span>
</div>
);
};
export default EducationInput;
Codesandbox

How do I reset modal props on close

I have a component, DeleteRoute, which is just a wrapper around a Modal. It allows me to delete a "Route" entity.
I am using React as well as Redux to maintain data and state.
I am struggling with the correct architecture to make the modal work. I didn't like having all of the state in the parent container because it made the parent cluttered. So I put the redux actions inside the modal. This way I pass in a route from the parent, but call "deleteRoute" from the redux store inside the DeleteRoute component, and I can display success and error messages.
This all works great, except it I close and re-open the modal, the previous success/error message is still displayed. This is because the closing/opening is done with a toggle from the parent, but the parent can't reset the child props.
I thought I could just pass the success and error properties in the parent, and anytime the parent re-renders it would reset those, but it isn't (despite it re-rendering when isOpen changes).
class DeleteRoute extends React.Component {
constructor(props) {
super(props);
this.deleteRoute = this.deleteRoute.bind(this);
}
deleteRoute() {
this.props.deleteRoute(this.props.route);
}
render() {
var route = this.props.route || {};
return (
<div>
<Modal
isOpen={this.props.isOpen}
toggle={this.props.toggle}
>
<ModalHeader toggle={this.props.toggle}>Delete Route</ModalHeader>
<ModalBody>
{this.props.isLoading && <Spinner color="primary" />}
<Alert color="danger" isOpen={this.props.error}>{this.props.error}</Alert>
<Alert color="success" isOpen={this.props.success}>Deleted successfully</Alert>
<Form>
<Row form>
<Col>
<FormGroup>
<Label for="CssPlatform">Css Platform</Label>
<Input disabled name="CssPlatform" type="text" value={route.CSSPlatform} />
</FormGroup>
</Col>
<Col>
<FormGroup>
<Label for="ProdManager">Prod Manager</Label>
<Input disabled name="ProdManager" type="text" value={route.ProdManager} />
</FormGroup>
</Col>
</Row>
<Row form>
<Col>
<FormGroup>
<Label for="CssProduct">Css Product</Label>
<Input disabled name="CssProduct" type="text" value={route.CSSProduct} />
</FormGroup>
</Col>
<Col>
<FormGroup>
<Label for="ProgSupervisor">Prog Supervisor</Label>
<Input disabled name="ProgSupervisor" type="text" value={route.ProgSupervisor} />
</FormGroup>
</Col>
</Row>
<Row form>
<Col>
<FormGroup>
<Label for="CssSubProduct">Css SubProduct</Label>
<Input disabled name="CssSubProduct" type="text" value={route.CSSSubProduct} />
</FormGroup>
</Col>
<Col>
<FormGroup>
<Label for="RallyProject">Rally Project</Label>
<Input disabled name="RallyProject" type="text" value={route.RallyProject} />
</FormGroup>
</Col>
</Row>
<Row form>
<Col>
<FormGroup check inline>
<Label check>
<Input disabled name="CssProductActive" type="checkbox" checked={route.CSSProductActive} />
Css Product Active
</Label>
</FormGroup>
</Col>
<Col>
<FormGroup check inline>
<Label check>
<Input disabled name="CssSubProductActive" type="checkbox" checked={route.CSSSubProductActive} />
Css SubProduct Active
</Label>
</FormGroup>
</Col>
</Row>
</Form>
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.deleteRoute}>Delete Route</Button>{' '}
<Button color="secondary" onClick={this.props.toggle}>Cancel</Button>
</ModalFooter>
</Modal>
</div>
);
}
}
export default connect(
state => state.deleteRouteReducer,
dispatch => bindActionCreators(actionCreators, dispatch))(DeleteRoute);
render() {
return (
<div>
<h2>Routes</h2>
<p>Routes define how items from CSS get imported into Rally. Use routes to connect a Rally project to a set of criteria in CSS.</p>
<div>
<AddRoute isOpen={this.props.showAddRoute} toggle={this.toggleAddRoute} />
<DeleteRoute error={this.props.deleteRouteError} success={this.props.deleteRouteSuccess} isOpen={this.props.showDeleteRoute} route={this.props.selectedRoute} toggle={this.toggleDeleteRoute} />
<DataTable
actions={[
{ Click: this.toggleAddRoute, Color: 'green', Icon: 'MdAdd', ToolTip: "Add new route" },
{ Click: this.toggleEditRoute, Color: 'orange', Icon: 'MdEdit', ToolTip: "Edit route", RowAction: true },
{ Click: this.toggleDeleteRoute, Color: 'red', Icon: 'MdRemove', ToolTip: "Delete route", RowAction: true },
]}
columns={[
{ Title: "Platform", Field: "CSSPlatform" },
{ Title: "Product", Field: "CSSProduct" },
{ Title: "SubProduct", Field: "CSSSubProduct" },
{ Title: "ProdManager", Field: "ProdManager" },
{ Title: "ProgSupervisor", Field: "ProgSupervisor" },
{ Title: "Product Active?", Field: "CSSProductActive" },
{ Title: "SubProduct Active?", Field: "CSSSubProductActive" },
{ Title: "Rally Project", Field: "RallyProject" },
{ Title: "Rally Url", Field: "RallyUrl" }
]}
data={this.props.routes}
edit={this.editRoute}
isLoading={this.props.isLoading} />
</div>
</div>
);
I ended up adding a redux action to reset the success and error props, and then calling that when the modal is closed. I think this is in line with correct redux architecture, but I'm open to better solutions if anyone has them.
class DeleteRoute extends React.Component {
constructor(props) {
super(props);
this.deleteRoute = this.deleteRoute.bind(this);
this.toggle = this.toggle.bind(this);
}
deleteRoute() {
this.props.deleteRoute(this.props.route);
}
toggle() {
if (this.props.isOpen) {
// reset the error and success messages on close
this.props.initialize();
}
this.props.toggle();
}
render() {
var route = this.props.route || {};
return (
<div>
<Modal
isOpen={this.props.isOpen}
toggle={this.toggle}
>
<ModalHeader toggle={this.toggle}>Delete Route</ModalHeader>
<ModalBody>
{this.props.isLoading && <Spinner color="primary" />}
<Alert color="danger" isOpen={this.props.error}>{this.props.error}</Alert>
<Alert color="success" isOpen={this.props.success}>Deleted successfully</Alert>
<Form>
<Row form>
<Col>
<FormGroup>
<Label for="CssPlatform">Css Platform</Label>
<Input disabled name="CssPlatform" type="text" value={route.CSSPlatform} />
</FormGroup>
</Col>
<Col>
<FormGroup>
<Label for="ProdManager">Prod Manager</Label>
<Input disabled name="ProdManager" type="text" value={route.ProdManager} />
</FormGroup>
</Col>
</Row>
<Row form>
<Col>
<FormGroup>
<Label for="CssProduct">Css Product</Label>
<Input disabled name="CssProduct" type="text" value={route.CSSProduct} />
</FormGroup>
</Col>
<Col>
<FormGroup>
<Label for="ProgSupervisor">Prog Supervisor</Label>
<Input disabled name="ProgSupervisor" type="text" value={route.ProgSupervisor} />
</FormGroup>
</Col>
</Row>
<Row form>
<Col>
<FormGroup>
<Label for="CssSubProduct">Css SubProduct</Label>
<Input disabled name="CssSubProduct" type="text" value={route.CSSSubProduct} />
</FormGroup>
</Col>
<Col>
<FormGroup>
<Label for="RallyProject">Rally Project</Label>
<Input disabled name="RallyProject" type="text" value={route.RallyProject} />
</FormGroup>
</Col>
</Row>
<Row form>
<Col>
<FormGroup check inline>
<Label check>
<Input disabled name="CssProductActive" type="checkbox" checked={route.CSSProductActive} />
Css Product Active
</Label>
</FormGroup>
</Col>
<Col>
<FormGroup check inline>
<Label check>
<Input disabled name="CssSubProductActive" type="checkbox" checked={route.CSSSubProductActive} />
Css SubProduct Active
</Label>
</FormGroup>
</Col>
</Row>
</Form>
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.deleteRoute}>Delete Route</Button>{' '}
<Button color="secondary" onClick={this.toggle}>Cancel</Button>
</ModalFooter>
</Modal>
</div>
);
}
}
export default connect(
state => state.deleteRouteReducer,
dispatch => bindActionCreators(actionCreators, dispatch))(DeleteRoute);
and the redux bits
import axios from 'axios';
// actions
const deleteRouteType = "DELETE_ROUTE";
const deleteRouteFailureType = "DELETE_ROUTE_FAILURE";
const deleteRouteSuccessType = "DELETE_ROUTE_SUCCESS";
const initializeType = "DELETE_ROUTE_INITIALIZE";
const initialState = { error: null, success: null };
// action creators
export const actionCreators = {
initialize: () => (dispatch) => {
dispatch({ type: initializeType });
},
deleteRoute: (route) => async (dispatch) => {
dispatch({ type: deleteRouteType });
axios
.delete(`api/route`, route)
.then(res => {
if (res.data.error) {
dispatch({ type: deleteRouteFailureType, payload: res.data.errorMessage });
}
else {
dispatch({ type: deleteRouteSuccessType, payload: res.data.data });
}
})
.catch(err => {
dispatch({ type: deleteRouteFailureType, payload: err.message });
});
}
};
// reducers
export const reducer = (state, action) => {
state = state || initialState;
switch (action.type) {
case initializeType:
return {
...state,
error: null,
isLoading: false,
success: false
};
case deleteRouteType:
return {
...state,
error: null,
isLoading: true,
success: false
};
case deleteRouteFailureType:
return {
...state,
error: action.payload,
isLoading: false,
success: false
};
case deleteRouteSuccessType:
return {
...state,
error: null,
isLoading: false,
success: true
};
default:
return state;
}
};

How to independently delete dynamically-added input fields in ReactJS

I'm trying to independently delete dynamic inputs in a form in React. I have a user attribute group, and then user attribute children. I need to be able to dynamically add new user attribute groups and children, but then delete those fields without deleting ALL of the child attributes.
Right now, when I delete a child attribute, it deletes one from EACH user attribute group.
I have a working fiddle here that shows my code: https://codesandbox.io/embed/23kr654w80
import React, { Component } from "react";
import { Button, Input, Row, Col, Form, FormGroup, Label } from "reactstrap";
class OfferCriteria extends Component {
constructor(props) {
super(props);
this.state = {
single: "",
attributeSingle: [{ single: "" }],
child: "",
attributeChild: [{ child: " " }]
};
}
handleNameChange = event => {
this.setState({
name: event.target.value
});
};
handleAddSingleAttribute = () => {
this.setState({
attributeSingle: this.state.attributeSingle.concat([{ name: "" }])
});
};
handleRemoveSingleAttribute = idx => () => {
this.setState({
attributeSingle: this.state.attributeSingle.filter(
(s, sidx) => idx !== sidx
)
});
};
handleAddChildAttribute = () => {
this.setState({
attributeChild: this.state.attributeChild.concat([{ child: "" }])
});
};
handleRemoveChildAttribute = idz => () => {
this.setState({
attributeChild: this.state.attributeChild.filter(sidz => idz !== sidz)
});
};
render() {
return (
<div>
<Row>
<Col lg="10">
<hr />
</Col>
<Col lg="2" className="float-right">
<Button color="success" onClick={this.handleAddSingleAttribute}>
Add Attribute Group
</Button>
</Col>
</Row>
{this.state.attributeSingle.map(() => (
<div>
<br />
<Row>
<Col lg="2">
<Label>User Attributes</Label>
</Col>
<Col lg="3" className="float-left">
<FormGroup check inline>
<Input
className="form-check-input"
type="radio"
id="includeUserAttributes"
name="inline-radios"
value="includeUserAttributes"
/>
<Label
className="form-check-label"
check
htmlFor="inline-radio1"
>
Include
</Label>
</FormGroup>
<FormGroup check inline>
<Input
className="form-check-input"
type="radio"
id="excludeUserAttributes"
name="inline-radios"
value="excludeUserAttributes"
/>
<Label
className="form-check-label"
check
htmlFor="inline-radio2"
>
Exclude
</Label>
</FormGroup>
</Col>
<Col lg="4">
<Input
type="text"
name="text-input"
placeholder="This is parent attribute"
/>
</Col>
</Row>
<br />
<Row>
<Col lg="3">
{this.state.attributeChild.map(() => (
<div className="shareholder">
<Input
type="text"
name="text-input"
placeholder="This is child attribute"
/>
</div>
))}
</Col>
<Col lg="3" className="float-right">
{this.state.attributeChild.map(() => (
<div className="shareholder">
<Button
color="primary"
onClick={this.handleAddChildAttribute}
>
Add Attribute Child
</Button>
<br />
</div>
))}
</Col>
<Col lg="3" className="float-right">
{this.state.attributeChild.map(idz => (
<div className="shareholder">
<Button
color="danger"
onClick={this.handleRemoveChildAttribute(idz)}
>
Remove Attribute Child
</Button>
<br />
</div>
))}
</Col>
</Row>
<hr />
</div>
))}
</div>
);
}
}
export default OfferCriteria;
I need these child attributes to delete ONLY in their parent attribute group, instead of deleting all of them from all the attribute groups.
There are a couple of things going wrong with your code, but I'll focus on your initial question.
The problem is that you use the same array of child for all your groups. In order to be correct, you should include the attributeChild state into the attributeSingle objects :
{
attributeSingle: [
{
single: "",
attributeChild: [
{
child: " "
}
]
}
]
}
That way, children remain independent between groups.

react+redux with react-bootstrap > FormControl > get value

I am using react+redux with react-bootstrap components.
I would like to pass the value of a FormControl text element (email) to the dispatched redux action but I do not know how to do that.
class LoginForm extends React.Component {
render() {
const email = React.findDOMNode(this.refs.email);
return (
<div>
<Form horizontal>
<FormGroup controlId="formHorizontalEmail">
<Col componentClass={ControlLabel}>Email</Col>
<Col><FormControl type="email" ref="email"/></Col>
</FormGroup>
<FormGroup>
<Col>
<Button type="submit" block>Sign in</Button>
</Col>
</FormGroup>
</Form>
<Button onClick={() => this.props.doLogin(email, 'password')}>Login</Button>
</div>
)
}
}
/**
* Connect staff.
*/
const mapStateToProps = (state) => {
return {
...
};
};
const mapDispatchToProps = (dispatch) => {
return {
doLogin: (email, password) => dispatch(performLogin(email, password))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm)
The only one way how to read text value of a FormControl with React (according to my research) is this:
class LoginForm extends React.Component {
handleOnChange = (event) => {
this.setState({ [event.target.id]: event.target.value }, null);
}
render() {
return (
<div>
<Form horizontal>
<FormGroup controlId="email">
<Col componentClass={ControlLabel}}>Email</Col>
<Col>
<FormControl type="email" placeholder="Email" onChange={this.handleOnChange}
/>
</Col>
</FormGroup>
<FormGroup controlId="password">
<Col componentClass={ControlLabel} sm={2}>Password</Col>
<Col>
<FormControl type="password" placeholder="Password" onChange={this.handleOnChange} />
</Col>
</FormGroup>
<FormGroup>
<Col>
<Button onClick={() => this.props.doLogin(this.state.email, this.state.password)}>Submit</Button>
</Col>
</FormGroup>
</Form>
</div>
)
}
}

Resources