I mapped over a list of objects to render table rows. Each table row has and input with type checkbox.
const [ isChecked, setIsChecked ] = useState(false);
const handleChange = (e) => {
setIsChecked(e.target.checked)
};
const tableRow = clonedUsers.map((user, index) => {
return (
<tr key={index}>
<td className='spacer-left' />
<td className='checkbox'>
<div className='pretty p-svg p-curve'>
<input type='checkbox' onChange={handleChange} checked={isChecked} />
<div className='state p-primary'>
<svg className='svg svg-icon' viewBox='0 0 20 20'>
<path
d='M7.629,14.566c0.125,0.125,0.291,0.188,0.456,0.188c0.164,0,0.329-0.062,0.456-0.188l8.219-8.221c0.252-0.252,0.252-0.659,0-0.911c-0.252-0.252-0.659-0.252-0.911,0l-7.764,7.763L4.152,9.267c-0.252-0.251-0.66-0.251-0.911,0c-0.252,0.252-0.252,0.66,0,0.911L7.629,14.566z'
style={{ stroke: 'white', fill: 'white' }}
/>
</svg>
<label />
</div>
</div>
</td>
<td className='team-name'>{user.name}</td>
<td className='team-lead-name d-none d-xl-flex'>{user.position}</td>
<td className='team-index d-none d-md-flex'>{user.email}</td>
<td className='team-lead-rating'>
<Link to='/people-settings'>Edit team</Link>
</td>
<td className='team-lead-rating'>
<Link to='/people-settings'>Deactivate user</Link>
</td>
</tr>
);
});
When I click on any checkbox, it is marking all of them as checked. What am I doing wrong here? Is there any better way of implementing this?
You have to extract the checkbox to a separate component or i would say you need to create a separate component to use a local state for it:
export default const Checkbox = props => {
const [isChecked, setIsChecked] = useState(false);
const handleChange = e => {
setIsChecked(!isChecked);
}
return (
<input type='checkbox' onChange={handleChange} checked={isChecked} />
);
}
Now you can use this:
<div className='pretty p-svg p-curve'>
<Checkbox />
What am I doing wrong here?
The problem is there's only one state used for all checkboxes.
Is there any better way of implementing this?
You can either use list or object to store the state for each checkbox.
But for quick lookup, I will use object with the following structure:
{ [name]: <checkbox value> }
const [checked, setChecked] = useState({});
const handleChange = (e, name) => {
setChecked({
...checked,
[name]: e.target.checked
})
};
<input type='checkbox'
onChange={(e) => handleChange(e, user.name)}
checked={checked[user.name]}
/>
I'd keep the checked ids in state e.g.:
const [checked, setChecked] = useState([]);
const handleChange = id => () => {
setChecked(prev => {
if (prev.includes(id)) {
return prev.filter(x => x !== id);
} else {
return [...prev, id];
}
});
};
Then in my input:
<input
type='checkbox'
onChange={handleChange(user.id)}
checked={checked.includes(user.id)}
/>
A cleaner way to write your handler is with lodash using xor.
import xor from 'lodash/xor';
///
const handleChange = id => () => {
setChecked(prev => xor(prev, [id]));
};
Related
For some reason when I submit a form I can't get the value of the checked boxes I don't mean the check value but the value attribute of the HTML element itself.
This is my element in the parent component :
function parent(props){
const [filterCriteria, setFilterCriteria] = useState([]);
const [companies, setCompanies] = useState([]);
const [categories, setCategories] = useState([]);
useEffect(() => {
axios
.get("http://localhost:4000/api/companies")
.then((res) => {
companiesData = res.data.companies;
setCompanies([...new Set(companiesData.map((item) => item.company))]);
})
.finally(() => {
setIslLoading(false);
});
}, []);
useEffect(() => {
axios
.get("http://localhost:4000/api/categories")
.then((res) => {
categoriesData = res.data.categories;
setCategories([
...new Set(categoriesData.map((item) => item.category)),
]);
})
.finally(() => {
setIslLoading(false);
});
}, []);
const onFilteringHandler = (e) => {
e.preventDefault();
//fun fact the e.target is looped useing "for of" not "for in" although it's an
object
for (const element of e.target) {
if (element.type === "checkbox" &&
element.checked === true &&
companies.includes(element.value)) {
setFilterCriteria([...filterCriteria, element.value]);
}
if (element.type === "checkbox" &&
element.checked === true &&
categories.includes(element.value)) {
setFilterCriteria([...filterCriteria, element.value]);
}
}
};
return (
//this element is in the medal of tother elements but for simplification i
removed them
<FilterBox
categoriesCriteria={categories}
settingFilterCriteria={onFilteringHandler}
companiesCriteria={companies}
/>
)
}
This is my element in the child "FilterBox" component :
function FilterBox(props) {
// this is to map the categories into check boxes with values, keys and label
let categoriesFilters = props.categoriesCriteria.map((category, index) => {
return (
<label key={index}>
{category}
//this is the value i'm tring to get in the form submition
<input type="checkbox" value={category} />
</label>
);
});
// this is to map the categories into check boxes with values, keys and label
let companiesFilters = props.companiesCriteria.map((company, index) => {
return (
<label key={index}>
{company}
//this is the value i'm tring to get in the form submition
<input type="checkbox" value={company} />
</label>
);
});
return (
<form
onSubmit={props.settingFilterCriteria}
className="product_creation_form"
>
<div>
<h5 className="filter_title">Categories</h5>
{categoriesFilters}
</div>
<div>
<h5 className="filter_title">Companies</h5>
{companiesFilters}
</div>
<button type="submit" className="form_button">
Apply
</button>
<button type="reset" className="form_button">
Clear All
</button>
</form>
);
}
Nowhere is the issue, I can't get the value in this line:
setFilterCriteria([...filterCriteria, element.value]);
Although the element.value exist on the e.target?
How to do that?
FilterBox Component
function FilterBox(props) {
const categoriesFilters = props.categoriesCriteria.map((category, index) => {
return (
<label key={index}>
{category}
<input type="checkbox" value={category} name={category} />
</label>
);
});
const companiesFilters = props.companiesCriteria.map((company, index) => {
return (
<label key={index}>
{company}
<input type="checkbox" value={company} name={company} />
</label>
);
});
return (
<form
onSubmit={props.settingFilterCriteria}
className="product_creation_form"
>
<div>
<h5 className="filter_title">Categories</h5>
{categoriesFilters}
</div>
<div>
<h5 className="filter_title">Companies</h5>
{companiesFilters}
</div>
<button type="submit" className="form_button">
Apply
</button>
<button type="reset" className="form_button">
Clear All
</button>
</form>
);
}
App Component:
function App() {
const [companies] = React.useState(["companyOne", "companyTwo"]);
const [categories] = React.useState(["categoryOne", "categoryTwo"]);
const onFilteringHandler = (e) => {
e.preventDefault();
const filterCriteria = { companies: [], categories: [] };
for (const element of e.target) {
if (element.type === "checkbox" && element.checked) {
if (companies.includes(element.value)) {
filterCriteria.companies.push(element.name);
}
if (categories.includes(element.value)) {
filterCriteria.categories.push(element.name);
}
}
}
console.log(filterCriteria);
};
return (
<FilterBox
categoriesCriteria={categories}
settingFilterCriteria={onFilteringHandler}
companiesCriteria={companies}
/>
);
}
I took the liberty to disconnect the initial API loading of the companies & categories react state arrays and hardcoded them for simplicity. The below code should be enough to send meaningful data to be able to filter out on the backend.
I took the approach with a non-controlled form state, as your code seemed to favour this kind of approach. You can try and keep the filter values in the react state too but I believe this complicates the solution a bit (especially that state update is async) and it seems that you only need to collect the current filters state value on a button press. If however, you need it for some other purposes, then a controlled form state would most likely be needed.
Сode in three files. In setList () you need to pass an array of objects to allocate, but they are generated using map. What is the right thing to do? in general I am trying to adapt my code to this https://codesandbox.io/s/react-select-all-checkbox-jbub2 But there the array for the Checkbox is moved to a separate file, and mine is generated using map.
https://codesandbox.io/s/sweet-butterfly-0s4ff?file=/src/TableBody/TableBody.jsx
1-file)
let Checkbox = () => {
return (
<div>
<label className={s.checkbox}>
<input className={s.checkbox__input} type="checkbox"/>
<span className={s.checkbox__fake}></span>
</label>
</div>
)
}
2-file)
const Tablehead = (handleSelectAll, isCheckAll ) => {
return (
<thead className = {s.header}>
<tr className = {s.area}>
<th ><Checkbox name="selectAll" id="selectAll" handleClick={handleSelectAll} isChecked={isCheckAll}/>
</th>
</tr>
</thead>
)
}
3-file)
const TableBody = ({droplets}) => {
const [isCheckAll, setIsCheckAll] = useState(false);
const [isCheck, setIsCheck] = useState([]);
const [list, setList] = useState([]);
useEffect(() => {
setList();
}, [list]);
const handleSelectAll = e => {
setIsCheckAll(!isCheckAll);
setIsCheck(list.map(li => li.id));
if (isCheckAll) {
setIsCheck([]);
}
};
const handleClick = e => {
const { id, checked } = e.target;
setIsCheck([...isCheck, id]);
if (!checked) {
setIsCheck(isCheck.filter(item => item !== id));
}
};
return (
<>
{droplets.map((droplet, index, id, name ) =>
<tr className={s.area} key={index} >
<td ><Checkbox key={id} name={name} handleClick={handleClick} isChecked={isCheck.includes(id)}/></td>
<td><button type="submit" className={s.button}><Edit /></button></td>
<td><button type="submit" className={s.button}><Trash /></button></td>
</tr>
)
}
</>
)
}
So there are several problems here
Component Checkbox doesn't take any props
const Tablehead = (handleSelectAll, isCheckAll) should be const Tablehead = ({ handleSelectAll, isCheckAll })
And most important one is your TableHead and TableBodyComponents both need this checkbox information so you need to lift your state up from TableBody to Table Component.
Also the example code you are following seems to do a lot of redundant things which are not necessary to implement your feature. Simply storing a checked property in each of your droplets should be enough and two functions to toggle individual and toggle all.
So I made the above changes in your code-sandbox link.
Here is the Link
I'm once again here asking for help, I have this component that renders an empty array item on mount, how can I avoid that and conditionally render a <p> tag that tells the user there's no items in the array yet? I have tried this code but it didn't work. This is my component (I know I have to refactor it, I just want to make it work first):
import React, { useState, useEffect } from "react"
const formatThousands = require("format-thousands")
const Clients = () => {
const [client, setClient] = useState({})
const [allClients, setAllClients] = useState([])
const [name, setName] = useState("")
const [cuota, setCuota] = useState("")
const [id, setId] = useState("")
useEffect(() => {
setAllClients([...allClients, client])
}, [client])
function handleSubmit(event) {
event.preventDefault()
if (!name || !cuota || !id) return
setClient({ ...client, name, cuota, id })
setName("")
setCuota("")
setId("")
}
function handleDeleteButton(id) {
allClients.filter(cl => id !== cl.id)
}
return (
<div className="container">
<form className="input-group" onSubmit={handleSubmit}>
<span className="input-group-text">Nuevo cliente</span>
<input
type="text"
className="form-control"
placeholder="Agregar un nuevo cliente"
value={name}
onChange={event => setName(event.target.value)}
/>
<input
type="number"
placeholder="Monto cuota"
className="form-control"
value={cuota}
onChange={event => setCuota(event.target.value)}
/>
<input
type="number"
placeholder="identificador"
className="form-control"
value={id}
onChange={event => setId(event.target.value)}
/>
<button className="btn btn-success" type="submit">
Agregar
</button>
</form>
<div className="text-center my-2">
<h3>Clientes</h3>
</div>
<table className="table table-striped">
<thead>
<tr>
<th scope="col">Cliente</th>
<th scope="col">Cuota</th>
<th scope="col">Identificador</th>
</tr>
</thead>
<tbody>
{allClients ? (
allClients.map(client => (
<tr key={client.id}>
<td>{client.name}</td>
<td>{formatThousands(client.cuota)}</td>
<td>{client.id}</td>
<td>
<button
onClick={() => handleDeleteButton(client.id)}
className="btn btn-danger btn-sm"
>
Borrar
</button>
</td>
</tr>
))
) : (
<p>Sin clientes</p>
)}
</tbody>
</table>
</div>
)
}
export default Clients
This is the output I get on mount:
This is the initial value of allClients, I don't know why Array [ {} ]
This is caused by the useEffect block. You add and empty object to the array the first time the component is rendered. That is why this condition allClients.length > 0 is true:
This is the initial value of allClients, I don't know why Array [ {} ]
Solution 1: Initialize allClients with null. Do the same with client. Check if client has value:
const [client, setClient] = useState(null)
const [allClients, setAllClients] = useState(null);
useEffect(() => {
if(client) setAllClients([...allClients || [], client]);
}, [client]);
Solution 2: Initialize only client with null and then check for allClients.length > 0 to render the elements.
const [client, setClient] = useState(null)
const [allClients, setAllClients] = useState([]);
useEffect(() => {
if(client) setAllClients([...allClients, client]);
}, [client]);
In both case you will need to check if client has a valid value.
This is your current code:
allClients ? allClients.map : <p></p>
In contrast, this is what your code should be:
allClients.length ? allClients.map : <p></p>
allClients is an empty array so it returns true. Instead, your code should check allClients.length which actually returns the length of the array.
I'm working on the edit/update the prices in the price list in my React app. I'm almost close but was unable to input any changes. Every time I tried to change the price in the input, I get an error saying "TypeError: onChange is not a function"...
I'm trying to write my code that's almost similar to this tutorial: https://medium.com/the-andela-way/handling-user-input-in-react-crud-1396e51a70bf#8858
So far, I was able to toggle the price between input field and back but I'm trying to edit the input to save any changes...What am I missing? I kept checking between my code and this tutorial to make sure everything is working...
Here's my code (functions) in the Parent component, PriceForm.js:
toggleItemEditing = index => {
this.setState({
priceArr: this.state.priceArr.map((item, itemIndex) => {
if (itemIndex === index) {
return {
...item,
isEditing: !item.isEditing
}
}
return item;
})
});
};
handlePriceUpdate = (event, index) => {
const target = event.target;
const value = target.value;
const number = target.number;
this.setState({
priceArr: this.state.priceArr.map((item, itemIndex) => {
if (itemIndex === index) {
return {
...item,
[number]: value
}
}
return item;
})
});
console.log("price update", event);
};
and where it's called in:
{this.state.priceArr.map((props, index) => (
<PriceBox
{...props}
key={props.date}
toggleEditing={this.toggleItemEditing}
handleDeletePrice={this.handleDeletePrice}
onChange={this.handlePriceUpdate}
/>
))}
And here's my code for the Child component, SinglePriceBox.js:
import React, { Component } from "react";
export default class SinglePricebox extends Component {
constructor(props) {
super(props);
this.state = {
isInEditMode: false,
todaydate: this.props.date
};
this.toggleEditPriceSubmission = this.toggleEditPriceSubmission.bind(this);
}
toggleEditPriceSubmission() {
this.setState(state => ({ isInEditMode: !state.isInEditMode }));
}
render() {
const { isInEditMode, onChange, index } = this.state;
return (
<div className="pricebox">
<article className="pricetable">
<table>
<tbody>
<tr>
<td className="date-width">{this.props.date}</td>
<td className="price-width">
{isInEditMode ? (
<input type="text" name="number" value={this.props.number} onChange={event => onChange(event, index)} />
) : (
this.props.number
)}
</td>
<td className="editing-btn">
<button
type="button"
className="edit-btn"
onClick={this.toggleEditPriceSubmission}
>
{isInEditMode ? "Save" : "Edit"}
</button>
</td>
<td>
{this.props.handleDeletePrice && (
<button
type="button"
className="delete-btn"
onClick={() => this.props.handleDeletePrice(this.props.date)}
>
X
</button>
)}
</td>
</tr>
</tbody>
</table>
</article>
</div>
);
}
}
You can check out my demo at https://codesandbox.io/s/github/kikidesignnet/caissa. You will be able to check out the error if you click on Prices button, then click on Edit button to change the price in the input field that appears.
import React, { Component } from "react";
export default class SinglePricebox extends Component {
constructor(props) {
super(props);
this.state = {
isInEditMode: false,
todaydate: this.props.date
};
this.toggleEditPriceSubmission = this.toggleEditPriceSubmission.bind(this);
this.handleChange = this.handleChange.bind(this);
}
toggleEditPriceSubmission() {
this.setState(state => ({ isInEditMode: !state.isInEditMode }));
}
handleChange = (e, index) => {
// write your code here
}
render() {
const { isInEditMode, index } = this.state;
return (
<div className="pricebox">
<article className="pricetable">
<table>
<tbody>
<tr>
<td className="date-width">{this.props.date}</td>
<td className="price-width">
{isInEditMode ? (
<input type="text" name="number" value={this.props.number} onChange={event => this.handleChange(event, index)} />
) : (
this.props.number
)}
</td>
<td className="editing-btn">
<button
type="button"
className="edit-btn"
onClick={this.toggleEditPriceSubmission}
>
{isInEditMode ? "Save" : "Edit"}
</button>
</td>
<td>
{this.props.handleDeletePrice && (
<button
type="button"
className="delete-btn"
onClick={() => this.props.handleDeletePrice(this.props.date)}
>
X
</button>
)}
</td>
</tr>
</tbody>
</table>
</article>
</div>
);
}
}
In the following line:
<input type="text" name="number" value={this.props.number} onChange={event => onChange(event, index)} />
You're calling this.state.onChange but there is no onChange in your state:
this.state = {
isInEditMode: false,
todaydate: this.props.date
};
After looking at your codesandbox, it seems that onChange is passed as a props to PriceBox, so you should do this in SinglePriceBox render():
const { isInEditMode, index } = this.state;
const { onChange } = this.props;
This will remove the error you were having, but the update still doesn't work because target.number is undefined in PriceForm.handlePriceUpdate :(
However target.name is defined and equal to 'number' which is a valid key in your price list
the problem is in how you pass your function as a prop. You should pass the function call in this way:
<PriceBox
{...props}
[...]
onChange={(e) => this.handlePriceUpdate(e, index)}
/>
Then call it in your child component:
<input
type="text"
[...]
onChange={event => this.props.onChange(event, index)}
/>
Also, I would not using index here, I would rather use an ID from your object instead
I have Form component which should get form elements like inputs, buttons etc. How i can detect onChange or onClick events or value if they passed like children? For example i change the second input and how detect that i change exactly second input but not another
I need to detect all actions in Form component
const Form = ({children, ...rest}) => {
const onChangeHandler = () => {
//detect change input
}
return (
<div style={{display: 'flex', flexWrap: 'wrap'}}>
{children}
</div>
)
}
const App = () => {
return <Form>
<input style={{width: '100%'}}/>
<input style={{width: '100%'}}/>
<input style={{width: '100%'}}/>
</Form>
}
ReactDOM.render(<App/>, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"/>
How about using the React.cloneElement method?
const Form = ({children, ...rest}) => {
const onChangeHandler = (e) => {
const value = e.target.value
const id = e.target.id
console.log(value, id)
}
return (
<div style={{display: 'flex', flexWrap: 'wrap'}}>
{React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
onChange: onChangeHandler
})
})}
</div>
)
}
const App = () => {
return <Form>
<input id="input1" />
<input id="input2" />
<input id="input3" />
</Form>
}
ReactDOM.render(<App/>, document.getElementById('root'))
Update: yes sorry it was untested, there's no way the input would know about the Form onChangeHandler so you just map it to the onChange in cloneElement.
I've added id attributes to each input so that you can see how the value for each input is change in the onChangeHandler. If you are wanting to save the value to state you can then use this id as a key in something like Redux.
React.cloneElement is what you need for this.
From the docs,
Clone and return a new React element using element as the starting
point. The resulting element will have the original element’s props
with the new props merged in shallowly. New children will replace
existing children. key and ref from the original element will be
preserved.
import React from 'react'
import ReactDOM from 'react-dom'
const Form = ({children, ...rest}) => {
const onChangeHandler = e => {
//detect change input
const value = e.target.value
const name = e.target.name
console.log('You have changed ', name, ' with value ', value)
}
return (
<div style={{display: 'flex', flexWrap: 'wrap'}}>
{React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
onChange: onChangeHandler,
name: `input${index + 1}`,
})
})}
</div>
)
}
const App = () => {
return (
<Form>
<input />
<input />
<input />
</Form>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
Demo