React - List?Dictionary?State? Or am i missing something - reactjs

Let's start off with the problem I'm having and telling you guys what I would like to achieve.
First of all, I'm getting this error
Warning: A component is changing a controlled input of type text to be uncontrolled.
Input elements should not switch from controlled to uncontrolled (or vice versa).
Decide between using a controlled or uncontrolled input element for the lifetime of the component.
My goal is to save data first to the state. Or I should use List, or dictionary? This is where I'm stuck. I will post my code here also for you to check what I'm doing wrong or what should I do differently.
import React from 'react'
import './TableData.css'
class TableData extends React.Component{
constructor(props){
super(props)
this.state = {
rows:[{service: '',
quantity: '',
price: '',
sum: ''}]
}
this.handleChange = this.handleChange.bind(this)
this.handleAddRow = this.handleAddRow.bind(this)
this.handleRemoveRow = this.handleRemoveRow.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleChange = idx => event => {
var rows = [...this.state.rows]
rows[idx] = {
[event.target.name]: event.target.value
}
this.setState({
rows
})
var data = this.state.rows
console.log("Log me", data)
}
handleAddRow = () => {
var item = {
service: '',
quantity: '',
price: '',
sum: ''
}
this.setState({
rows: [...this.state.rows, item]
})
}
handleRemoveRow = () => {
this.setState({
rows: this.state.rows.slice(0, -1)
})
}
handleSubmit = (event) => {
event.preventDefault()
var tableData = this.state.rows
console.log("Final data is:", tableData)
}
render() {
return (
<div className="tablePos container" >
<form onSubmit={this.handleSubmit}>
<div className="row">
<table id="tab_logic">
<thead className="tableBackground">
<tr>
<th className="col-md-auto"> Service </th>
<th className="col col-lg-2"> Quantity </th>
<th className="col col-lg-2"> Price </th>
<th className="col col-lg-2"> Sum </th>
</tr>
</thead>
<tbody>
{this.state.rows.map((item, idx) => (
<tr key={idx}>
<td>
<input className="form-control" type="text" name="service" placeholder="Ex: Cloud Service" value={this.state.rows[idx].service} onChange={this.handleChange(idx)}/>
</td>
<td>
<input className="form-control" type="text" name="quantity" placeholder="Ex: 2 Month" value={this.state.rows[idx].quantity} onChange={this.handleChange(idx)}/>
</td>
<td>
<input className="form-control" type="text" name="price" placeholder="Ex: 75.00" value={this.state.rows[idx].price} onChange={this.handleChange(idx)}/>
</td>
<td>
<input className="form-control" type="text" name="sum" placeholder="Ex: 150.00" value={this.state.rows[idx].sum} onChange={this.handleChange(idx)} />
</td>
</tr>
))}
</tbody>
</table>
</div>
<button>Send Data!</button>
</form>
<button onClick={this.handleAddRow} className="btn btn-success">Add Row</button>
<button onClick={this.handleRemoveRow} className="btn btn-danger">Delete Last Row</button>
</div>
);
}
}
export default TableData
So basically it creates 4 input boxes and then you can write in and if you are done you click Send Data it saves it to state or add new row and then it will add new row for you to input data. What I do get is the following from that code.
Console log picture of the data
It only saves the last input field data when I click send data not all of them.
Sorry about my messy explanation but I hope you did understand my problem and thank you for your replies!

while assigning the values inside onChange. You are spreading the array as needed. But you have to spread the object too.. Otherwise, it will just assign the last-changed-input-field-value to the object.
rows[idx] = {
...this.state.rows[idx],
[event.target.name]: event.target.value
};
You can find my code below.
https://codesandbox.io/s/small-dew-wjqqi

Related

Weird return of data after splice and setstate

Here's simulate in codesandbox
https://codesandbox.io/embed/epic-nash-mxteu?fontsize=14&hidenavigation=1&theme=dark
I am having a weird behavior when I remove a row from the dynamic rows created.
I have removed the row with 2 b. As you can see the console log has the correct data while the UI showing wrong data. It means the function works well but somehow displaying incorrectly.
Anyone have idea why? Thanks in advance
Screenshots
Before remove row
After remove row
Source code
const [gavRows, setGAVRows] = useState([]);
const handleGAVAddRow = async () => {
try {
const item = {
gav_field: '',
gav_value: ''
};
setGAVRows([...gavRows, item]);
} catch (error) {
console.log('error', error)
}
};
const handleGAVRemoveSpecificRow = (idx) => {
console.log('idx', idx)
const tempRows = [...gavRows];
console.log('tempRows', tempRows)
tempRows.splice(idx, 1);
setGAVRows(tempRows)
};
const handleGAVChange = async (idx, e) => {
const { name, value } = e.target;
var tempRows = [...gavRows];
tempRows[idx][name] = value;
setGAVRows(tempRows)
};
<table className="customgav_section">
<tbody>
{
gavRows.map((item, idx) => {
console.log('map idx', idx, item)
return (
<tr key={idx}>
<td>
<Input type="text"
name="gav_field" id="gav_field"
value={gavRows[idx].field}
onChange={(e) => handleGAVChange(idx, e)}
/>
</td>
<td>
<Input type="text"
name="gav_value" id="gav_value"
value={gavRows[idx].value}
onChange={(e) => handleGAVChange(idx, e)}
/>
</td>
<td>
<Button outline color="danger" onClick={() => handleGAVRemoveSpecificRow(idx)}><FaMinus /></Button>
</td>
</tr>)
})
}
</tbody>
</table>
Your problem is that you are using the index of the array as the key.
Read why that is bad: https://robinpokorny.medium.com/index-as-a-key-is-an-anti-pattern-e0349aece318
A quick hack was assigning a random number as the key of each item in the gavRows and using that as the key of the element. See updated code: https://codesandbox.io/s/charming-bouman-03zn7
Also, the id of an element must be unique in the DOM so i removed those from the input elements.
Codesandbox
The problem is you put the wrong input value.
Remember the item object you set is:
const item = {
gav_field: "", //not field
gav_value: "" //not value
};
You should modify code from
<td>
<Input
type="text"
name="gav_field"
id="gav_field"
value={gavRows[idx].field}
onChange={(e) => handleGAVChange(idx, e)}
/>
</td>
<td>
<Input
type="text"
name="gav_value"
id="gav_value"
value={gavRows[idx].value}
onChange={(e) => handleGAVChange(idx, e)}
/>
</td>
To:
<td>
<Input
type="text"
name="gav_field"
id="gav_field"
value={gavRows[idx].gav_field}
onChange={(e) => handleGAVChange(idx, e)}
/>
</td>
<td>
<Input
type="text"
name="gav_value"
id="gav_value"
value={gavRows[idx].gav_value}
onChange={(e) => handleGAVChange(idx, e)}
/>
</td>

How to draw a dynamic table with inputs for the user in some rows?

I want to draw a table with a list of items, and have inputs for each of the items. I managed to draw the table and render the items from a get request. The goal is to take inputs for each item in different rows. However, since the items are rendered using a map function, when the user types something in an input, it gets rendered in all the rows. I need it to get rendered only on its respective row. I understand why it is happening but I dont know how to do it. Im new to React so some help would be appreciated.
Code for rendering the table
<div className="d-flex justify-content-center card card-body border border-5 border-primary">
<div className="table table-striped text-primary table-container">
<thead className="border border-light">
<tr>
<th>Nombre</th>
<th>Sabor</th>
<th>Precio</th>
<th>Cantidad</th>
<th>Empaque</th>
<th>Instrucciones Adicionales</th>
</tr>
</thead>
<tbody>
{sweets.map(sweet => (
<tr key={sweet.sweet_id}>
<td>{sweet.s_name}</td>
<td>{sweet.s_flavor}</td>
<td>{sweet.s_price}</td>
<td>
<input
type="text"
onChange={e => set_sweet_quantity(e.target.value)}
value={sweet_quantity}
className="form-control"
/>
</td>
<td>
<input
type="number"
onChange={e => set_sweet_package(e.target.value)}
value={sweet_package}
className="form-control"
/>
</td>
<td>
<input
type="text"
onChange={e => set_additional_instructions(e.target.value)}
value={additional_instructions}
className="form-control"
placeholder='N/A'
/>
</td>
<td>
<button className="btn btn-primary btn-sm btn-block"
onClick={() => handleItemSweets(sweet.sweet_id)}
>
AƱadir a la orden
</button>
<button
className="btn btn-danger btn-sm btn-block btn-delete"
>
Remover
</button>
</td>
</tr>
))}
</tbody>
</div>
</div>
Code for managing the inputs and the get request
const handleItemSweets = async (id) => {
set_sweet_id(id)
console.log(sweet_id)
const res = await fetch(API + '/create_sweet_item', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
sweet_quantity,
additional_instructions,
sweet_package,
sweet_id
})
})
const data = await res.json();
console.log(data);
set_sweet_quantity('');
set_additional_instructions('');
set_sweet_package('');
set_sweet_id('');
}
// Variable para guardar los dulces en una lista desde el json
const [sweets, set_sweets] = useState([]);
const getSweets = async () => {
const res = await fetch(API + "/get_all_sweets");
const data = await res.json();
set_sweets(data);
console.log(data);
};
[Error visualization][1]
[1]: https://i.stack.imgur.com/ldd1x.png
Instead of storing just one sweet_quantity state, you can convert it to an object to store all sweet_quantities:
const [sweet_quantities, set_sweet_quantities] = useState({})
and when you receive the API response, you can populate that object with the default quantity value associated with the sweet's id:
const getSweets = async () => {
const res = await fetch(API + "/get_all_sweets");
const data = await res.json();
set_sweets(data);
...
// Set default quantities in state for each item
const quantities = data.reduce((a, b) => ({ ...a, [b.sweet_id]: b.quantity}), {})
set_sweet_quantities(quantities)
};
after that you can then reference the selected state by the sweet's id and update it accordingly:
{sweets.map((sweet) => (
<tr key={sweet.sweet_id}>
<td>{sweet.s_name}</td>
<td>{sweet.s_flavor}</td>
<td>{sweet.s_price}</td>
<td>
<input
type="text"
onChange={e => set_sweet_quantities({...sweet_quantities, [sweet.sweet_id]: e.target.value})}
value={sweet_quantities[sweet.sweet_id]}
className="form-control"
/>
</td>
....
You can then repeat this logic for the other values.

I'm trying to display search results for both name and genres for particular TV shows in React, how to use one filter method for both name and genre?

My table.js file below. I've managed to search and filter through my table in order to display search results for show by name, but not sure how to implement a second condition so that the same search bar can be used to display searches for shows by genre.
Genres is an array, like so: genres: ["Drama", "Action", "Adventure"]
As I said previously, I'm able to filter through shows using the user input to find shows by name, but I'd like to use that same filter function to also display results by genre. Any help would be hugely appreciated.
export default class Table extends Component {
constructor(props) {
super(props);
this.state = {
searchByTitle: ""
};
this.handleChange = this.handleChange.bind(this)
}
handleChange(event) {
this.setState({searchByTitle : event.target.value})
}
renderRow(show) {
return (
<tr key={show.id}>
<td className="border border-green-600">
{show.image && (
<img className="w-full" src={show.image.medium} alt={show.name} />
)}
</td>
<td className="border border-green-600 bg-green-100">{show.name}</td>
<td className="border border-green-600 bg-green-100">
{show.genres.join(", ")}
</td>
<td className="border border-green-600 bg-green-100">{show.runtime}</td>
<td className="border border-green-600 bg-green-100">
{show.network && show.network.name}
</td>
<td className="border border-green-600 bg-green-100">{show.status}</td>
</tr>
);
}
render() {
const filterShows = (shows, query) => {
if (!query) {
return shows;
}
return shows.filter((show) => {
const showName = show.name.toLowerCase();
// const showGenres = show.genres.join(", ")
return showName.includes(query)
})
}
const filteredShows = filterShows(this.props.shows, this.state.searchByTitle)
return (
<table className="table-fixed border-collapse">
<thead>
<tr>
<input
type="text"
id="header-search"
placeholder="Search shows"
name="search"
onChange={this.handleChange}
/>
</tr>
<tr>
<th className="w-1/12">Poster</th>
<th className="w-4/12">Name</th>
<th className="w-4/12">Genres</th>
<th className="w-1/12">Runtime</th>
<th className="w-1/12">Network</th>
<th className="w-1/12">Status</th>
</tr>
</thead>
<tbody>{filteredShows.map((show) => this.renderRow(show))}</tbody>
</table>
);
}
}
Nevermind, I got it! Thank you to anyone who tried to help.
Just had change up my filterShows function so that it joined the array and then used toLowerCase(). See below:
const filterShows = (shows, query) => {
if (!query) {
return shows;
}
return shows.filter((show) => {
const showName = show.name.toLowerCase();
const showGenres = show.genres.join(", ").toLowerCase();
// const showGenre = show.genres;
return showName.includes(query) || showGenres.includes(query);
});
};

How to update many input with one handler in ReactJs?

I'm new at React, I have 3 years experience with Angular, which is why React seems strange to me. I created many input like this:
<tr>
<td>
<input value={this.state.x}
onChange={this.handleChange}/>
</td>
<td>
<input value={this.state.y}
onChange={this.handleChange}/>
</td>
<td>
<input value={this.state.z}
onChange={this.handleChange}/>
</td>
</tr>
from what I learn so far, I had to handle this input change event one by one for each <input> which I find laborious. Can I write one function to update all the input above ? Like:
handleChange = (event) => {
let obj = {};
obj[key] = event.target.value; // the key is my variable name, eg: x, y, z
this.setState(obj)
}
Does it possible to give several input a single handler function ? Thanks in advance
You can use event.target.name and event.target.value in order to update your component's as long as you set name property in each input:
this.setState({
[event.target.name]: event.target.value,
})
class App extends React.Component {
constructor() {
super()
this.state = {
x: '',
y: '',
z: '',
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(event) {
this.setState({
[event.target.name]: event.target.value,
})
}
render() {
return (
<div>
<table>
<tr>
<td>
<input name="x" value={this.state.x} onChange={this.handleChange} />
</td>
<td>
<input name="y" value={this.state.y} onChange={this.handleChange} />
</td>
<td>
<input name="z" value={this.state.z} onChange={this.handleChange} />
</td>
</tr>
</table>
<div>Current state: {JSON.stringify(this.state)}</div>
</div>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
One other way to achieve this without giving names to your inputs is to bind names of the properties in your state as arguments in your change handlers. Then in the event handler you will receive the name of the property as the first argument and the event as the second argument. Here is an example:
class Test extends React.Component {
state = {
x: '',
y: '',
z: ''
}
handleChange(name, event) {
this.setState({ [name]: event.target.value });
}
render() {
return (<table>
<tr>
<td>
<input value={this.state.x}
onChange={this.handleChange.bind(this, 'x')} />
</td>
<td>
<input value={this.state.y}
onChange={this.handleChange.bind(this, 'y')} />
</td>
<td>
<input value={this.state.z}
onChange={this.handleChange.bind(this, 'z')} />
</td>
</tr>
</table>);
}
}
This way is probably less pretty than giving names to your inputs, but it works if you for some reason don't want to have names on the inputs.
handleChange = () => {
const {value, name} = event.target
this.setState({
[name] : value
})
}

How to set State when value is derived from Array in React App

I have a react App, which renders a table from an array in the state.
I am trying to have an "EDIT MODE" which transforms some fields into textbox's so that I can update each row in the table.
I'm not sure how I can handle the onChange event when the value is derived from an element inside an array in the state.
here is my code, I have explained the problem in the comments:
class AddProjectType extends Component {
constructor(props) {
super(props);
this.state = {
editMode: false
}
this.changeEditMode = this.changeEditMode.bind(this); //Bind This so I can use this.setState
this.changeProjectName = this.changeProjectName.bind(this); //Bind This so I can use this.setState
}
componentDidMount() {
this.props.fetchProjectTypes(); //This fetched the table from the nodejs server
}
changeEditMode() {
this.setState({ editMode: !this.state.editMode }); //Convert into edit mode and change rows in the table to inputs
}
changeProjectName(event){
//this.setState - Unsure how to set state of a particular array HERE IS THE PROBLEM
}
render() {
if (!this.props.projectTypes) {
return (
<CenterLoader /> //loading the table from server - show loader
)
}
else
return (
<div className="container">
<table className="ProjectType-Table">
<tbody>
<tr>
<th>
Id
</th>
<th>
Project Type
</th>
<th>
</th>
</tr>
{this.props.projectTypes.map((projectType, i) => { //RENDER ALL ROWS
return (
<tr key={i}>
<td>
{projectType._id}
</td>
<td>
{this.state.editMode ?
<input type="text" className="browser-default" defaultValue={projectType.name} onChange={this.changeProjectName}/> //On change, I need to save back to the state this value
:
projectType.name
}
</td>
<td>
<button className="btn btn-small mr-1" onClick={this.changeEditMode}>Edit</button>
<button className="btn btn-small">Delete</button>
</td>
</tr>
)
}
)}
</tbody>
</table>
</div>
);
}
}
function mapStateToProps(state) {
return { projectTypes: state.quizz.projectTypes };
}
export default connect(mapStateToProps, actions)(AddProjectType);
Attach data-idx={i} in the input like this,
<input type="text" className="browser-default" defaultValue={projectType.name} onChange={this.changeProjectName} data-idx={i}/>
In your changeProjectName(event) handler,
var index = event.target.dataset.idx; // Here you will get index

Resources