My component like this:
import React, { useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import './SizeAndColor.css';
const initialData = {
sizes: ['S', 'M', 'L', 'XL'],
data: [
{
rowId: uuidv4(),
color: 'White',
sizes: [
{ fieldId: uuidv4(), sizeName: 'S', qty: 10 },
{ fieldId: uuidv4(), sizeName: 'M', qty: 20 },
{ fieldId: uuidv4(), sizeName: 'L', qty: 30 },
{ fieldId: uuidv4(), sizeName: 'XL', qty: 40 }
]
},
{
rowId: uuidv4(),
color: 'Gray',
sizes: [
{ fieldId: uuidv4(), sizeName: 'S', qty: 50 },
{ fieldId: uuidv4(), sizeName: 'M', qty: 60 },
{ fieldId: uuidv4(), sizeName: 'L', qty: 70 },
{ fieldId: uuidv4(), sizeName: 'XL', qty: 80 }
]
}
]
};
const initialFieldValue = {
id: 0,
initialData: initialData
};
export default function SizeAndColor() {
const [state, setState] = useState(initialFieldValue);
const onChange = (e, rowId, fieldId) => {
const { name, value } = e.target;
const updatedData = state.initialData.data.map(row => {
if (rowId === row.rowId) {
row.sizes.map(item => {
if (fieldId === item.fieldId) {
if (!isNaN(value)) {
item[name] = value;
}
}
return item;
});
}
return row;
});
setState({ ...state, initialData: { ...initialData, data: updatedData } });
};
return (
<div>
<table>
<thead>
<tr>
<td></td>
{state.initialData.sizes.map(size => (
<td key={uuidv4()}>{size}</td>
))}
</tr>
</thead>
<tbody>
{state.initialData.data.map(item => (
<tr key={uuidv4()}>
<td>{item.color}</td>
{item.sizes.map(size => (
<td key={uuidv4()}>
<input
type="text"
name="qty"
value={size.qty}
onChange={e => {
onChange(e, item.rowId, size.fieldId);
}}
/>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
When I start typing on any input field, It work only first key press. Than again have to click the input field. Than again after first key press, can not type anything.
I tried it with different scenario. If my initialData object doesn't have nested property, It works fine.
how do i solve this issue, please help me.
Issue
You are generating new React keys each render.
<div>
<table>
<thead>
<tr>
<td></td>
{state.initialData.sizes.map(size => (
<td key={uuidv4()}>{size}</td> // <-- here
))}
</tr>
</thead>
<tbody>
{state.initialData.data.map(item => (
<tr key={uuidv4()}> // <-- here
<td>{item.color}</td>
{item.sizes.map(size => (
<td key={uuidv4()}> // <-- here
<input
type="text"
name="qty"
value={size.qty}
onChange={e => {
onChange(e, item.rowId, size.fieldId);
}}
/>
</td>
))}
</tr>
))}
</tbody>
</table>
When you generate a new React key each render cycle it tells React that these are new components, so the previous ones are unmounted and a new component is instantiated. This is why the inputs lose focus.
Solution
Use the ids generated in your data so the React keys are stable, i.e. they don't change from render to render.
<div>
<table>
<thead>
<tr>
<td></td>
{state.initialData.sizes.map((size, index) => (
// use index since size array likely doesn't change
<td key={index}>{size}</td>
))}
</tr>
</thead>
<tbody>
{state.initialData.data.map(item => (
<tr key={item.rowId}> // <-- use row id
<td>{item.color}</td>
{item.sizes.map(size => (
<td key={size.fieldId}> // <-- use field id
<input
type="text"
name="qty"
value={size.qty}
onChange={e => {
onChange(e, item.rowId, size.fieldId);
}}
/>
</td>
))}
</tr>
))}
</tbody>
</table>
The reason for that is that you generate random keys for input on each render, so React thinks it's a new unrelated field that doesn't need to be focused, you need to replace key={uuidv4()} with key={item.rowId} and key={size.fieldId}
Try to remove the returns, the setState should do the work.
Related
Im creating an invoice generator where the user can add an item, its price, and the quantity. I want to access the user inputs as a state from a child functional component (TableItems.js) into a parent functional component (TableSheet.js) to be able to save the user inputs into a database preferably firestore. I'm having a problem accessing the user input value from the child component to the parent component. I have been struggling with this bug for days, i really hope you guys could help me.
This is the Child component
import React, {useState, useEffect} from 'react'
function TableItems({index, tableItem }) {
const [price, setPrice] = useState(0);
const [qty, setQty] = useState(0);
const [total, setTotal] = useState([]);
useEffect(() => {
//arithmetically add price and qty values
const x = Number(price) * Number(qty)
setTotal(x)
return () => {
//clean up function will be here
};
}, [price, qty, total ]);
return (
<>
<tr>
<td><input type='text' required/></td>
<td><input type='number' value={price} onChange={(e) => setPrice(e.target.value)}/></td>
<td><input type='number' value={qty} onChange={(e) => setQty(e.target.value)}/></td>
<td>{total}</td>
</tr>
</>
)
}
export default TableItems
This is the Parent component
import React, { useState } from 'react'
import TableItems from './TableItems'
function TableSheet() {
const [tableItem, setTableItem] = useState([1]);
//adding a new table cell (table row)
const addCell = () => {
setTableItem((t) => [...t, t + 1])
}
return (
<div>
<table>
<thead>
<th>Item Description</th>
<th>Price</th>
<th>Qty.</th>
<th>Total</th>
</thead>
{
tableItem.map((tableItem, index, setItem) => {
return <TableItems key={index} tableItem={tableItem} setItem={setItem} addCell={addCell}/>
})
}
</table>
<button onClick={addCell}>+</button>
</div>
)
}
export default TableSheet
You tableItem state should contains item objects (quantity and price)
TableItems
function TableItems({ index, tableItem, onChangeItem }) {
return (
<>
<tr>
<td>
<input type="text" required />
</td>
<td>
<input
type="number"
value={tableItem.price}
onChange={(e) => onChangeItem(index, "price", e.target.value)}
/>
</td>
<td>
<input
type="number"
value={tableItem.quantity}
onChange={(e) => onChangeItem(index, "quantity", e.target.value)}
/>
</td>
<td>{Number(tableItem.price) * Number(tableItem.quantity)}</td>
</tr>
</>
);
}
TableSheet
function TableSheet() {
const [tableItem, setTableItem] = useState([
{
price: 0,
quantity: 0
}
]);
const onChangeItem = (index, type, value) => {
const newTable = tableItem.map((item, idx) => {
if (idx === index)
return {
...item,
[type]: value
};
return item;
});
setTableItem(newTable);
};
const addCell = () => {
setTableItem((t) => [
...t,
{
price: 0,
quantity: 0
}
]);
};
const totalPrice = tableItem.reduce((acc, cur) => {
acc += Number(cur.price) * Number(cur.quantity);
return acc;
}, 0);
return (
<div>
<table>
<thead>
<th>Item Description</th>
<th>Price</th>
<th>Qty.</th>
<th>Total</th>
</thead>
{tableItem.map((tableItem, index) => {
return (
<TableItems
key={index}
index={index}
tableItem={tableItem}
onChangeItem={onChangeItem}
/>
);
})}
</table>
<button onClick={addCell}>+</button>
<div>Total: {totalPrice}</div>
</div>
);
}
you can check in my codesandbox. Hope it help!
I have this data:
const data = [
{
_id: '1',
status: 'active',
user: {
email: 'one#mail.com',
provider: 'google',
profile: {
image: 'https://example.com/image1.jpg',
},
},
},
{
_id: '2',
status: 'inactive',
user: {
email: 'two#mail.com',
provider: 'github',
profile: {
image: 'https://example.com/image2.jpg',
},
},
},
]
const head = ['_id', 'status', 'email', 'image']
const body = ['_id', 'status', 'user.email', 'user.profile.image']
I want to display in the table dynamically only show the string in the body array.
I tried and it worked _id, and status but not the string which contains dot
Here is what I have tried:
data.map((item) => (
<tr key={item._id}>
{body.map((i, index) => (
<td key={index}>{item[i]}</td>
))}
</tr>
))
here is my way to achieve your desire output.
use this function
function getDeepObjValue (item, s) {
return s.split('.').reduce((p, c) => {
p = p[c];
return p;
}, item);
};
use it like this
data.map((item) => {
return (
<tr key={item._id}>
{body.map((keys, i) => {
return <td key={i}>{getDeepObjValue(item, keys)}</td>;
})}
</tr>
);
})
and if you want to check demo click here
If your body data is not dynamic you can do :
data.map((item) => (
<tr key={item._id}>
<>
<td key={index}>{item._id}</td>
<td key={index}>{item.status}</td>
<td key={index}>{item.user.email}</td>
<td key={index}>{item.user.profile.image}</td>
<>
</tr>
))
else you can use
const deeper = (data, array) => {
if(array.length > 1){
return deeper(data[array[0]], array.slice(1, array.length))
} else {
return data[array[0]]
}
}
with
data.map((item) => (
<tr key={item._id}>
{body.map((i, index) => (
<td key={index}>{deeper(item, i.split('.'))}</td>
))}
</tr>
))
Something like that
So I've created this Table component, intended to be reusable.
But I have this problem where the data didn't fill all the row correctly. It stacked up on first two rows.
Here's my code so far:
Menu.js
export const AddMenu = () => {
const theadData = ["No", "Name", "Category", "Price"];
const tbodyData = [
{
id: 1,
items: [1, "Hamburger", "Fast Food", 150],
},
{
id: 2,
items: [2, "Pizza", "Fast Food", 100],
},
];
return (
<div className="container">
<h1>Add Menu</h1>
<Table theadData={theadData} tbodyData={tbodyData} />
</div>
);
};
Table.js
export const Table = ({
theadData,
tbodyData,
}) => {
return (
<div className="table">
<table>
<thead>
<tr>
{theadData.map((item) => {
return <th title={item}>{item}</th>;
})}
</tr>
</thead>
<tr>
{tbodyData.map((item) => {
return <td key={item}>{item.items}</td>;
})}
</tr>
</tbody>
</table>
</div>
);
};
Thanks btw, hope to get an answer.
Add second map to loop over rows, and then loop over cells, like this:
{
tbodyData.map((row, index) => (
<tr key={index}>
{row.map((item) => {
return <td key={item}>{item}</td>;
})}
</tr>
));
}
In what way I can check/uncheck all checkboxes if in the set of data there is any boolean field and I'm not using react-table? For single checkbox is working but not for all. Thank you for any tip. I named checkboxes as corresponding name of the data in the table but it is not working.
This is my class:
class Boxes extends React.Component {
constructor(props) {
super(props);
this.state = {
datas: [
{name: "name1", status: 1},
{name: "name3", status: 0},
{name: "name2", status: 2},
],
checkboxes: [],
selectAll: true,
};
this.toggleAllChange = this.toggleAllChange.bind(this);
this.toggleOneChange = this.toggleOneChange.bind(this);
}
toggleAllChange(event) {
let checked = event.target.checked;
console.log(checked);
this.setState({
checkAll: checked,
checkboxes: document.getElementsByClassName("chbx")
});
for (let i in this.state.checkboxes){
let nameAct = this.state.checkboxes.item(i).name;
let checkbox = document.getElementsByName(nameAct);
this.setState({
[nameAct]: checked
});
}
}
toggleOneChange(event) {
const target = event.target;
const value = target.checked;
const name = target.name;
this.setState({
[name]: value
});
}
And my Table in render method:
<div className="input-group mb-3">
<InputGroup className="mb-3">
<input
type="checkbox"
value="checkAll"
defaultChecked={this.state.selectAll}
onChange={this.toggleAllChange.bind(this)}/>
Check/Uncheck All
</InputGroup>
</div>
<Table responsive>
<thead className="text-primary">
<tr>
<th>Data name</th>
<th>Data status</th>
<th>Check</th>
</tr>
</thead>
<tbody>
{this.state.datas.map(data => (
<tr>
<td>{data.name}</td>
<td>{data.status}</td>
<td>
<input
className="chbx"
type="checkbox"
id={"chbx" + data.name}
name={"chbx" + data.name}
defaultChecked={this.state.selectAll}
onChange={this.toggleOneChange.bind(this)}
/>
</td>
</tr>
))}
</tbody>
</Table>
Look at the following example, I've:
Used an object checkBoxObj to manage the state of checkboxes
Used the checked attribute instead of defaultChecked and checkBoxObj:
<input
className="chbx"
type="checkbox"
id={"chbx" + data.name}
name={"chbx" + data.name}
checked={this.state.checkBoxObj["chbx" + data.name]}
onChange={this.toggleOneChange.bind(this)}
/>
Change toggleAllChange and toggleOneChange methods
Live example:
const datas = [
{name: "name1", status: 1},
{name: "name3", status: 3},
{name: "name2", status: 2},
];
const selectAll = true;
const checkBoxObj = datas.reduce((acc, d) => (acc["chbx" + d.name] = selectAll, acc), {});
class Boxes extends React.Component {
constructor(props) {
super(props);
this.state = {
datas,
//allChecked: true,
selectAll,
checkBoxObj,
};
this.toggleAllChange = this.toggleAllChange.bind(this);
this.toggleOneChange = this.toggleOneChange.bind(this);
}
toggleAllChange(event) {
const selectAll = event.target.checked;
const newCheckBoxObj = {};
for(let k in this.state.checkBoxObj) {
newCheckBoxObj[k] = selectAll;
}
this.setState({
checkBoxObj: newCheckBoxObj,
selectAll
});
}
toggleOneChange(event) {
const target = event.target;
const value = target.checked;
const name = target.name;
const newCheckBoxObj = { ...this.state.checkBoxObj, [name]: value };
const selectAll = Object.values(newCheckBoxObj).every(chk => chk);
this.setState({
checkBoxObj: newCheckBoxObj,
selectAll
});
}
render() {
const filteredDatas = this.state.datas;
return (
<div>
<div className="input-group mb-3">
<input
type="checkbox"
value="checkAll"
style={{ marginRight: 5 }}
checked={this.state.selectAll}
onChange={this.toggleAllChange.bind(this)}
/>{" "}
Check/Uncheck All
</div>
<table responsive>
<thead className="text-primary">
<tr>
<th>Data name</th>
<th>Data status</th>
<th>Check</th>
</tr>
</thead>
<tbody>
{filteredDatas.map(data => (
<tr>
<td>{data.name}</td>
<td>{data.status}</td>
<td>
<input
className="chbx"
type="checkbox"
id={"chbx" + data.name}
name={"chbx" + data.name}
checked={this.state.checkBoxObj["chbx" + data.name]}
onChange={this.toggleOneChange.bind(this)}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>)
}
}
ReactDOM.render(<Boxes />, document.getElementById('root'));
<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="root"></div>
I have a table where there is one input box in each row. There are 3 rows in total and i need to calculate the total from those three input boxes value. But the state of value is not updating. I only get the initial state of value. For example, there is a state object of agent, hotel, admin. If i initialize the agent value 10, i get 10 in input box but when i try to change the value i only get 10. The value does not gets updated.
Here is the code
const Tbody = ({ roles, states, onChange, onBlur }) => {
const row = roles.map((role, index) => (
<tr key={index}>
<td>{index + 1}</td>
<td>{role.label}</td>
<td>
<TextFieldGroup
id="formControlsText"
type="number"
name={role.name}
value={states[role.name]}
onChange={event => onChange(event)}
onBlur={event => onBlur(event)}
error={states.errors[role.name]}
required
/>
</td>
</tr>
));
return <tbody>{row}</tbody>;
};
class Commission extends React.PureComponent {
state = {
agentCommission: 0,
hotelCommission: 0,
adminCommission: 0,
errors: {},
isSubmitted: false
};
handleChange = event => {
console.log(event.target);
const fieldName = event.target.name;
this.setState(
{ [event.target.name]: parseFloat(event.target.value) },
() => {
this.validateField([fieldName]);
}
);
};
handleBlur = event => {
const fieldName = event.target.name;
this.validateField([fieldName]);
};
validateField = validate => {
const errors = { ...this.state.errors };
let hasError = false;
validate.forEach(field => {
if (
parseFloat(this.state[field]) > 100 ||
parseFloat(this.state[field]) < 0
) {
hasError = true;
errors[field] = 'cannot be less than 0 and more than 100';
} else {
errors[field] = '';
}
});
this.setState({ errors });
return !hasError;
};
render() {
const { agentCommission, adminCommission, hotelcommission } = this.state;
const totalCommission = agentCommission + adminCommission + hotelcommission;
console.log('totalCommission', totalCommission);
return (
<div className="table-responsive">
<table className="table table-striped table-bordered table-condensed">
<thead>
<tr>
<th>S.N</th>
<th>Role</th>
<th>Commission</th>
</tr>
</thead>
<Tbody
roles={[
{ name: 'agentCommission', label: 'Agent' },
{ name: 'hotelCommission', label: 'Hotel Owner' },
{ name: 'adminCommission', label: 'Admin' }
]}
states={{ ...this.state }}
onChange={this.handleChange}
onBlur={this.handleBlur}
/>
<tbody>
<tr>
<td>
<button
className="btn btn-primary"
onClick={this.handleSubmit}
disabled={totalCommission === 100 ? false : true}>
Save Changes
</button>
</td>
</tr>
</tbody>
</table>
</div>
);
}
}
In ReactJS, when you extend a React component class, you must initialize the state in the constructor. Also, you need to call the parent class' constructor via super(props). This is the only way that the React library's class can get access to your state values, as well as provide access in methods such as setState()
https://codepen.io/julianfresco/pen/ybrZNe/
class Commission extends React.PureComponent {
constructor(props, context) {
super(props)
this.state = {
agentCommission: 0,
hotelCommission: 0,
adminCommission: 0,
errors: {},
isSubmitted: false
};
// method instance bindings
this.handleChange = this.handleChange.bind(this)
this.handleBlur = this.handleBlur.bind(this)
this.validateField = this.validateField.bind(this)
}
// ...
// you had 1 typo in the render function, hotelCommission wasn't camel case
render() {
const { agentCommission, adminCommission, hotelCommission } = this.state;
// ...
}
}
The issue is the Commission class, where you are not initializing the state.
Your code should be like the following:
const Tbody = ({ roles, states, onChange, onBlur }) => {
const row = roles.map((role, index) => (
<tr key={index}>
<td>{index + 1}</td>
<td>{role.label}</td>
<td>
<input
id="formControlsText"
type="number"
name={role.name}
value={states[role.name]}
onChange={event => onChange(event)}
onBlur={event => onBlur(event)}
error={states.errors[role.name]}
required
/>
</td>
</tr>
));
return <tbody>{row}</tbody>;
};
class Commission extends React.PureComponent {
constructor(props) {
super(props)
this.state = {
agentCommission: 0,
hotelCommission: 0,
adminCommission: 0,
errors: {},
isSubmitted: false
};
}
handleChange(event) {
console.log(event.target);
const fieldName = event.target.name;
this.setState(
{ [event.target.name]: parseFloat(event.target.value) },
() => {
this.validateField([fieldName]);
}
);
};
handleBlur(event) {
const fieldName = event.target.name;
this.validateField([fieldName]);
};
validateField(validate) {
const errors = { ...this.state.errors };
let hasError = false;
validate.forEach(field => {
if (
parseFloat(this.state[field]) > 100 ||
parseFloat(this.state[field]) < 0
) {
hasError = true;
errors[field] = 'cannot be less than 0 and more than 100';
} else {
errors[field] = '';
}
});
this.setState({ errors });
return !hasError;
};
render() {
const { agentCommission, adminCommission, hotelcommission } = this.state;
const totalCommission = agentCommission + adminCommission + hotelcommission;
console.log('totalCommission', totalCommission);
return (
<div className="table-responsive">
<table className="table table-striped table-bordered table-condensed">
<thead>
<tr>
<th>S.N</th>
<th>Role</th>
<th>Commission</th>
</tr>
</thead>
<Tbody
roles={[
{ name: 'agentCommission', label: 'Agent' },
{ name: 'hotelCommission', label: 'Hotel Owner' },
{ name: 'adminCommission', label: 'Admin' }
]}
states={{ ...this.state }}
onChange={this.handleChange}
onBlur={this.handleBlur}
/>
<tbody>
<tr>
<td>
<button
className="btn btn-primary"
onClick={this.handleSubmit}
disabled={totalCommission === 100 ? false : true}>
Save Changes
</button>
</td>
</tr>
</tbody>
</table>
</div>
);
}
}
Fiddle demo: https://codesandbox.io/s/KO3vDRGjR