How to update dynamic multiple input (user can add those input themself)? - reactjs

I have a form. Initially there is some default values (user name and address). When user click add, there is an extra input which user can enter another name and address, and the extra name and address will store in additionConfigs.
Example:
https://codesandbox.io/s/elastic-pateu-2uy4rt
import "./styles.css";
import { useState, useEffect } from "react";
export default function App() {
const [value, setValue] = useState([]);
const [additionConfigs, setAdditionConfigs] = useState([]);
useEffect(() => {
setTimeout(() => {
setValue([
{
id: 1,
baseName: "XXX",
config: {
name: "Kenny",
address: "New york"
}
},
{
id: 2,
baseName: "YYY",
config: {
name: "Ben",
address: "Boston"
}
},
{
id: 3,
baseName: "ZZZ",
config: {
name: "Mary",
address: "Los Angeles"
}
}
]);
}, 1000);
}, []);
const onAddBaseConfig = (item) => {
setAdditionConfigs((preValue) => [
...preValue,
{
id: item.id,
config: {
name: "",
address: ""
}
}
]);
};
console.log(additionConfigs);
const onChangeName = (e, id) => {
setAdditionConfigs((preValue) => {
const newValue = preValue.map((v) => {
if (v.id === id) {
return {
...v,
config: {
...v.config,
name: e.target.value
}
};
}
return v;
});
return newValue;
});
};
const onChangeAddress = (e, id) => {
setAdditionConfigs((preValue) => {
const newValue = preValue.map((v) => {
if (v.id === id) {
return {
...v,
config: {
...v.config,
address: e.target.value
}
};
}
return v;
});
return newValue;
});
};
return (
<>
{value.length > 0 &&
value.map((v, index) => (
<div className="item" key={index}>
<div className="item">
{v.config.name}
{v.config.address}
{additionConfigs.length > 0 &&
additionConfigs
.filter((config) => config.id === v.id)
.map((config) => (
<div>
<label>name</label>
<input
value={config.config.name}
onChange={(e) => onChangeName(e, config.id)}
/>
<label>address</label>
<input
value={config.config.address}
onChange={(e) => onChangeAddress(e, config.id)}
/>
</div>
))}
</div>
<button onClick={() => onAddBaseConfig(v)}>Add</button>
</div>
))}
</>
);
}
Currently, I use config.id to update the extra name and address, but there is an issue that if user add two or more extra name and address input, when updating the first one, the second will update, too.
How do I update respectively? Giving each group of input a flag?

Assuming that the component should not modify the base value as it is set by a useEffect, but keep a additionConfigs which need to support any amount of config inputs, perhaps one solution could be to make additionConfigs state an object.
The additionConfigs object could have id from base value as key and an array of configs as value, and perhaps each config need its own id, so that they can be controlled by the added input, without major refactor of the existing code structure.
Forked live with modifications: codesandbox
Perhaps try the following as an example:
Define additionConfigs state as an object:
const [additionConfigs, setAdditionConfigs] = useState({});
Update logic for additionConfigs when adding a config input:
(The id logic here is only adding previous id++, and should probably be replaced by a unique id generator in actual project)
const onAddBaseConfig = (item) => {
setAdditionConfigs((preValue) => {
const preConfigs = preValue?.[item.id];
const newId = preConfigs
? preConfigs.reduce((acc, cur) => (cur.id > acc ? cur.id : acc), 0) + 1
: 1;
return {
...preValue,
[item.id]: preConfigs
? [
...preConfigs,
{
id: newId,
config: {
name: "",
address: ""
}
}
]
: [
{
id: newId,
config: {
name: "",
address: ""
}
}
]
};
});
};
Update logic for a config input for name, a baseId is added as an argument as each base value can have multiple configs:
const onChangeName = (e, id, baseId) => {
setAdditionConfigs((preValue) => {
const newArr = preValue[baseId].map((v) => {
if (v.id === id) {
return {
...v,
config: {
...v.config,
name: e.target.value
}
};
}
return v;
});
return { ...preValue, [baseId]: newArr };
});
};
Same but for address:
const onChangeAddress = (e, id, baseId) => {
setAdditionConfigs((preValue) => {
const newArr = preValue[baseId].map((v) => {
if (v.id === id) {
return {
...v,
config: {
...v.config,
address: e.target.value
}
};
}
return v;
});
return { ...preValue, [baseId]: newArr };
});
};
Output with the changes:
<>
{value.length > 0 &&
value.map((v, index) => (
<div className="item" key={index}>
<div className="item">
{v.config.name}
{v.config.address}
{additionConfigs?.[v.id] &&
additionConfigs?.[v.id].length > 0 &&
additionConfigs?.[v.id].map((config, index) => (
<div key={config.id}>
<label>name</label>
<input
value={config.config.name}
onChange={(e) => onChangeName(e, config.id, v.id)}
/>
<label>address</label>
<input
value={config.config.address}
onChange={(e) => onChangeAddress(e, config.id, v.id)}
/>
</div>
))}
</div>
<button onClick={() => onAddBaseConfig(v)}>Add</button>
</div>
))}
</>

Related

A items List in React looses its all new data rows when edited previous rows, Using custom Uncontrolled Input component

Quick explanation of problem in this image. It's kind of product list where last row is empty and on filling quantity column a new row should appear. Problem is when you have added few new rows and you go back try changing previous rows the newly created rows disappears beyond that point where you made the change.
SANDBOX with all code replicating issue > https://codesandbox.io/s/react-hooks-playground-forked-xbkrub
Index (Main entry point)
import * as React from "react";
import { useState, useEffect } from "react";
import { render } from "react-dom";
import Input from "./Input";
function App() {
const [products, setProducts]: any = useState([]);
const getProductsAsync = () =>
new Promise((resolve, reject) => {
setTimeout(() => {
const prodsArray = [
{
id: 371337,
barcode: null,
name: "4x Cheese",
details: null,
tags: null,
unit: null,
productTimestamp: "2022-01-26T11:40:34.000Z",
activeDate: null,
deleteDate: null,
price: 1
},
{
id: 372093,
barcode: "",
name: "Capri Sun",
details: "",
tags: "",
unit: null,
productTimestamp: "2022-05-11T13:43:37.000Z",
activeDate: "2022-05-11T13:43:37.000Z",
deleteDate: null,
categories: { "924": "Juices", "13524": "3 for £11" },
price: 1
},
{
id: 372552,
barcode: "",
name: "Hot Chocolate",
details: "",
tags: "",
unit: null,
productTimestamp: "2022-05-25T11:15:40.000Z",
activeDate: "2022-05-25T11:15:40.000Z",
deleteDate: null,
categories: { "924": "Juices" },
price: 1
},
{
id: 372553,
barcode: "",
name: "Orange",
details: "",
tags: "",
unit: null,
productTimestamp: "2022-05-25T11:16:06.000Z",
activeDate: "2022-05-25T11:16:06.000Z",
deleteDate: null,
categories: { "924": "Juices" },
price: 1
},
{
id: 372598,
barcode: "",
name: "PRODUCT",
details: "",
tags: "",
unit: null,
productTimestamp: "2022-06-06T14:22:00.000Z",
activeDate: "2022-06-06T14:22:00.000Z",
deleteDate: null,
price: 1
}
];
resolve(prodsArray);
}, 1000);
});
useEffect(() => {
(async () => {
const productObjects: any = await getProductsAsync();
//add a new row at the end for adding more data.
addNewEmptyRow(productObjects);
})();
}, []);
useEffect(() => {}, [products]);
const onPriceChange = (index, event) => {
if (event !== undefined && event.target !== undefined) {
//make sure you supply name="fieldName you want to target in your html tag i.e <input name="taxPercentage">"
const fieldName = event.target.getAttribute("name");
const inputValue = event.target.value;
if (inputValue !== undefined && inputValue !== null) {
const prodComps = products.map((item, currIndex) => {
if (currIndex === index) {
let row = { ...item };
row[fieldName] = inputValue;
return row;
} else {
return item;
}
});
setProducts([...prodComps]);
addNewEmptyRow([...prodComps]);
}
}
};
const addNewEmptyRow = (list: any, rowToAdd?: any) => {
const lastRow = rowToAdd || list[list.length - 1];
if (
lastRow.price !== undefined &&
lastRow.price !== null &&
lastRow.price.toString().length > 0
) {
// enableCrossButtonForLastRow(list);
const rows: any = [
...list,
{
price: "",
vatPercentage: "",
tags: "",
name: " ",
id: null,
autoGenIdForNewlyAddedRow: Date.now(),
showCross: false
}
];
setProducts(rows);
}
};
const generateList: any = () => {
return products.map((item: any, index) => {
return (
<React.Fragment key={item.id || item.autoGenIdForNewlyAddedRow}>
<div>
<input type="text" name="name" value={item.name} readOnly={true} />
</div>
<div>
<Input
type="text"
name="price"
value={item.price}
onChange={(e) => onPriceChange(index, e)}
/>
</div>
<div>X</div>
</React.Fragment>
);
});
};
return (
<div>
<div
style={{
display: "grid",
gridTemplateColumns: "[nameOverride] 175px [price] 155px [cross] 65px"
}}
>
{generateList()}
</div>
</div>
);
}
render(<App />, document.getElementById("root"));
Input.tsx (Custom input component)
import * as React from "react";
import { useEffect, useRef, useState } from "react";
const Input = (props) => {
const [value, setValue] = useState("");
const inputRef: any = useRef(null);
const valueRef = useRef("");
useEffect(() => {
inputRef.current.addEventListener("input", handleChange);
}, []);
useEffect(() => {
if (Boolean(props.value)) {
//this logic sets the data to 2 decimal places
if (props.decimalplaces) {
const val = parseFloat(props.value).toFixed(props.decimalplaces);
valueRef.current = val;
setValue(val);
} else {
valueRef.current = props.value;
setValue(props.value);
}
}
}, [props]);
const handleChange = (e) => {
const val = e.target.value;
if (props.decimalplaces && val.match(/\./g)) {
const [, decimal] = val.split(".");
// restrict value to only 2 decimal places
if (decimal?.length > props.decimalplaces) {
inputRef.current.value = valueRef.current;
} else {
valueRef.current = e.target.value;
}
} else {
valueRef.current = val;
}
if (Boolean(props.onChange)) {
//this builds the custom event object similar to the javascript event object.
props.onChange(
buildEventObject({
value: valueRef.current,
name: props.name,
type: props.type
})
);
}
};
const buildEventObject = (params) => {
return {
target: {
value: params.value,
getAttribute: (key) => {
return params[key];
}
}
};
};
return (
<div>
<input type={props.type} ref={inputRef} defaultValue={value} />
</div>
);
};
export default Input;

Is there a way to handle state in a form that is dynamically built based off of parameters sent from the back end

I have a page in react 18 talking to a server which is passing information about how to build specific elements in a dynamic form. I am trying to figure out how to manage state in a case where there are multiple selects/multiselects in the page. Using one hook will not work separately for each dropdown field.
Code is updated with the latest updates. Only having issues with setting default values at this point. Hooks will not initially set values when given.
import { Calendar } from 'primereact/calendar';
import { Dropdown } from 'primereact/dropdown';
import { InputSwitch } from 'primereact/inputswitch';
import { InputText } from 'primereact/inputtext';
import { MultiSelect } from 'primereact/multiselect';
import React, { useEffect, useState, VFC } from 'react';
import { useLocation } from 'react-router-dom';
import { useEffectOnce } from 'usehooks-ts';
import { useAppDispatch, useAppSelector } from 'redux/store';
import Form from '../components/ReportViewForm/Form';
import { getReportParamsAsync, selectReportParams } from '../redux/slice';
export const ReportView: VFC = () => {
const location = useLocation();
const locState = location.state as any;
const dispatch = useAppDispatch();
const reportParams = useAppSelector(selectReportParams);
const fields: JSX.Element[] = [];
const depList: any[] = [];
//const defaultValList: any[] = [];
//dynamically setting state on all dropdown and multiselect fields
const handleDdlVal = (name: string, value: string) => {
depList.forEach((dep) => {
if (name === dep.dependencies[0]) {
dispatch(getReportParamsAsync(currentMenuItem + name + value));
}
});
setState((prev: any) => {
return { ...prev, [name]: value };
});
};
//dynamically setting state on all calendar fields
const handleCalVal = (name: string, value: Date) => {
setState((prev: any) => {
return { ...prev, [name]: value };
});
};
//dynamically setting state on all boolean fields
const handleBoolVal = (name: string, value: boolean) => {
setState((prev: any) => {
return { ...prev, [name]: value };
});
};
/* function getInitVals(values: any) {
const defaultList: any[] = [];
values.forEach((param: any) => {
defaultList.push({ name: param.name, value: param.defaultValues[0] });
});
} */
const [state, setState] = useState<any>({});
const [currentMenuItem, setCurrentMenuItem] = useState(locState.menuItem.id.toString());
useEffectOnce(() => {}), [];
useEffect(() => {
if (reportParams?.length === 0) {
dispatch(getReportParamsAsync(currentMenuItem));
}
//reload hack in order to get page to load correct fields when navigating to another report view
if (currentMenuItem != locState.menuItem.id) {
window.location.reload();
setCurrentMenuItem(locState.menuItem.id.toString());
}
}, [dispatch, reportParams, currentMenuItem, locState, state]);
//dependency list to check for dependent dropdowns, passed to reportddl
reportParams.forEach((parameter: any) => {
if (parameter.dependencies !== null && parameter.dependencies[0] !== 'apu_id') {
depList.push(parameter);
}
});
//filter dispatched data to build correct fields with data attached.
reportParams.forEach((parameter: any, i: number) => {
if (parameter.validValuesQueryBased === true) {
if (parameter.validValues !== null && parameter.multiValue) {
const dataList: any[] = [];
parameter.validValues.map((record: { value: any; label: any }) =>
dataList.push({ id: record.value, desc: record.label }),
);
fields.push(
<span key={i} className='p-float-label col-12 mx-3 field'>
<MultiSelect
options={dataList}
name={parameter.name}
value={state[parameter.name]}
onChange={(e) => handleDdlVal(parameter.name, e.value)}
></MultiSelect>
<label className='mx-3'>{parameter.prompt.substring(0, parameter.prompt.indexOf(':'))}</label>
</span>,
);
} else if (parameter.validValues !== null) {
const dataList: any[] = [];
parameter.validValues.map((record: { value: any; label: any }) =>
dataList.push({ id: record.value, desc: record.label }),
);
fields.push(
<span key={i} className='p-float-label col-12 mx-3 field'>
<Dropdown
options={dataList}
optionValue='id'
optionLabel='desc'
name={parameter.name}
onChange={(e) => handleDdlVal(parameter.name, e.value)}
value={state[parameter.name]}
//required={parameter.parameterStateName}
placeholder={'Select a Value'}
></Dropdown>
<label className='mx-3'>{parameter.prompt.substring(0, parameter.prompt.indexOf(':'))}</label>
</span>,
);
}
} else if (parameter.parameterTypeName === 'Boolean') {
fields.push(
<span key={i} className='col-12 mx-3 field-checkbox'>
<InputSwitch
checked={state[parameter.name]}
id={parameter.id}
name={parameter.name}
onChange={(e) => handleBoolVal(parameter.name, e.value)}
></InputSwitch>
<label className='mx-3'>{parameter.prompt.substring(0, parameter.prompt.indexOf(':'))}</label>
</span>,
);
} else if (parameter.parameterTypeName === 'DateTime') {
//const date = new Date(parameter.defaultValues[0]);
fields.push(
<span key={i} className='p-float-label col-12 mx-3 field'>
<Calendar
value={state[parameter.name]}
name={parameter.name}
onChange={(e) => {
const d: Date = e.value as Date;
handleCalVal(parameter.name, d);
}}
></Calendar>
<label className='mx-3'>{parameter.prompt.substring(0, parameter.prompt.indexOf(':'))}</label>
</span>,
);
} else if (parameter.name === 'apu_id') {
return null;
} else {
fields.push(
<span key={i} className='p-float-label col-12 mx-3 field'>
<InputText name={parameter.name}></InputText>
<label className='mx-3'>{parameter.prompt.substring(0, parameter.prompt.indexOf(':'))}</label>
</span>,
);
}
});
const onSubmit = () => {
console.log(state);
};
return (
<Form onReset={null} onSubmit={onSubmit} initialValues={null} validation={null} key={null}>
{fields}
</Form>
);
};
enter code here

How can I disable/gray out dropdown selection checkboxes for two records available in setSubmittedNominees state?

How can I disable/gray out dropdown selection checkboxes for two records available in a state setSubmittedNominees in react hooks ? I tried to pass into submittedNominees into selectedValues and disablePreSelectedValues(true) but it doesn't work that way can someone please advise on this ?
codesandbox link:
https://codesandbox.io/s/magical-haslett-s0oeh?file=/src/App.js
import React, { useRef, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { useForm } from "react-hook-form";
import Axios from "axios";
import { Link, useHistory } from "react-router-dom";
import Multiselect from "multiselect-react-dropdown";
const options = [
{ id: 1, name: "Ath", email: "ath.best#test1.com", access: null },
{ id: 2, name: "Arnolds", email: "arnold#test1.com", access: null },
{ id: 3, name: "Alter", email: "alloop#test1.com", access: null },
{ id: 4, name: "Brandan", email: "brandan#test1.com", access: null },
{ id: 5, name: "Ron", email: "ron#test1.com", access: null },
{ id: 6, name: "Rads", email: "rad#test1.com", access: null },
{ id: 7, name: "Sam", email: "sam#y.com", access: null }
];
const submitted = [
{ id: 4, name: "Brandan", email: "brandan#test1.com", access: null },
{ id: 5, name: "Ron", email: "ron#test1.com", access: null }
];
const Selection= () => {
const [option, setOption] = useState([]);
const [selectedOption, setSelectedOption] = useState([]);
const [nomRegister, setNomRegister] = useState([{}]);
const [helperText, setHelperText] = useState("");
const [userEmail, setUserEmail] = useState("");
const [submittedNominees, setSubmittedNominees] = useState([{}]);
const {
register,
handleSubmit,
watch,
formState: { errors },
reset
} = useForm();
const maxOptions = 3;
const history = useHistory();
useEffect(() => {
const userEmail = localStorage.getItem("loginEmail");
setUserEmail(userEmail);
});
useEffect(() => {
const fetchData = async () => {
try {
const res = await Axios.get(
"http://localhost:8000/service/nomineeslist"
);
//const data1 = res.data;
setOption(options);
console.log("Get the list of nominees :" + JSON.stringify(res.data));
} catch (e) {
console.log(e);
}
};
fetchData();
}, []);
useEffect(() => {
const fetchData = async () => {
try {
// const res = await Axios.get(
// "http://localhost:8000/service/submittednominees"
// );
setSubmittedNominees(submitted);
} catch (e) {
console.log(e);
}
};
fetchData();
}, []);
const handleTypeSelect = (e, i) => {
const copy = [...selectedOption];
copy.push(e[i]);
setSelectedOption(copy);
};
const sendNomination = () => {
console.log("What the Array holds: " + JSON.stringify(nomRegister));
const fetchData = async (nomRegister) => {
try {
const res = await Axios.post(
"http://localhost:8000/service/nominateperson",
{ userEmail },
nomRegister
);
if (res.data) {
console.log("Print data:" + res.data);
const successMessage = res.data.message;
setHelperText(successMessage);
setNomRegister(reset);
}
} catch (e) {
console.log(e);
setNomRegister(reset);
setHelperText(e.message);
}
};
fetchData();
};
options.forEach((option) => {
option.displayValue = option.name + "\t" + option.email;
submittedNominees.forEach((item) => {
let subEmail = item.email; // how can I pass those two email to selectedValues and make it disable ?
});
});
const handleChange = (e, i) => {
const { name, email, value } = e.target;
// immutating state (best practice)
const updateList = nomRegister.map((item) => {
return { ...item };
});
const select_Email = selectedOption.map((item) => {
return item.email;
});
//change the specific array case depends on the id //email:emailList[i],
updateList[i] = {
...updateList[i],
name: name,
email: select_Email[i],
reason: value
};
setNomRegister(updateList);
};
return (
<div className="App">
<h1>Nominate a person</h1>
<div className="nomineeSelectBox">
<div id="dialog2" className="triangle_down1" />
<div className="arrowdown">
<Multiselect
onSelect={(e) => handleTypeSelect(e, selectedOption.length)}
options={selectedOption.length + 1 === maxOptions ? [] : options}
displayValue="displayValue"
selectedValues={subEmail}
showCheckbox={true}
emptyRecordMsg={"Maximum nominees selected !"}
/>
</div>
</div>
<div className="nominationcount"></div>
<form onSubmit={handleSubmit(sendNomination)}>
<div className="nomineesSelectedList">
<h4>Selected Nominees</h4>
{selectedOption.map((x, i) => (
<div key={i}>
<div className="row eachrecord">
<div className="column">
<label className="nomlabel">
{x?.name} <b>>></b>
</label>
</div>
<input
required
type="textarea"
placeholder="Please provide reason for nomination.."
key={i}
id={i}
name={x?.name}
className="nomineechoosed"
onChange={(e) => handleChange(e, i)}
/>
</div>
</div>
))}
<div className="row">
<div className="buttongroup">
<input id="Submit" type="submit" value="Submit" />
<input id="Cancel" type="button" value="Cancel" />
</div>
</div>
</div>
</form>
<span className="nominationValidationText">{helperText}</span>
</div>
);
};
export default Selection;
the issue here is that you are showing your dropdown selection checkboxes by displayValue (displayValue="displayValue") and your submitted array that you you will assign to submittedNominees (setSubmittedNominees(submitted)) does not contain displayValue:
const submitted = [
{ id: 4, name: "Brandan", email: "brandan#test1.com", access: null },
{ id: 5, name: "Ron", email: "ron#test1.com", access: null }
];
Solution::
Make your submitted for example like this:
const submitted = [
{
id: 4,
name: "Brandan",
email: "brandan#test1.com",
access: null,
displayValue: "Brandan brandan#test1.com"
},
{
id: 5,
name: "Ron",
email: "ron#test1.com",
access: null,
displayValue: "Ron ron#test1.com"
}
];
Add selectedValues={submittedNominees} to your Multiselect to find out which values you will disable:
<Multiselect
onSelect={(e) => handleTypeSelect(e, selectedOption.length)}
options={selectedOption.length + 1 === maxOptions ? [] : options}
displayValue="displayValue"
disablePreSelectedValues={true}
selectedValues={submittedNominees}
showCheckbox={true}
emptyRecordMsg={"Maximum nominees selected !"}
/>
Codesandbox link

React losing checked while filtering

I cannot figure out how to add 'active' into the state of users.
For the sake of posting this here I hardcoded some users in the state, but they're supposed to be fetched from an API - and this doesn't come with 'active'. I need to be able to mark the checkboxes so that the specific user becomes active, also if active - it has to stay active when doing searches in the list through the text-input, so it doesn't reset. With what I wrote I am getting undefined for user.active. Any suggestions?
App.js
import './App.css';
import UserList from './components/UserList';
import './style/style.css';
function App() {
return (
<div className="App">
<UserList />
</div>
);
}
export default App;
UserList.js
import React, { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([
{
id: 681,
first_name: 'James',
last_name: 'Smith',
email: 'example1',
gender: 'm',
},
{
id: 3123,
first_name: 'Richard',
last_name: 'Greene',
email: 'example2',
gender: 'm',
},
{
id: 512,
first_name: 'Denise',
last_name: 'Bank',
email: 'example3',
gender: 'f',
},
{
id: 654,
first_name: 'Carl',
last_name: 'Ross',
email: 'example4',
gender: 'm',
},
]);
const [search, setSearch] = useState('');
const [filteredUsers, setFilteredUsers] = useState();
const [checkedUsers, setCheckedUsers] = useState('');
useEffect(() => {
const fetchUsers = async () => {
try {
const result = await fetch(users);
result.sort(function (a, b) {
if (a.last_name.toLowerCase() < b.last_name.toLowerCase()) return -1;
if (a.last_name.toLowerCase() > b.last_name.toLowerCase()) return 1;
else return;
});
setUsers(result);
} catch (error) {
console.log(error);
}
};
fetchUsers();
}, []);
useEffect(() => {
setFilteredUsers(
users.filter(
(user) =>
user.first_name.toLowerCase().includes(search.toLowerCase()) ||
user.last_name.toLowerCase().includes(search.toLowerCase())
)
);
}, [users, search]);
useEffect(() => {
setCheckedUsers(users.filter((user) => user.active));
}, [users]);
const changeChecked = (id) => {
users.forEach((user) => {
if (user.id === id) {
console.log(user);
if (user.active === '') {
user.active = true;
} else user.active = false;
}
});
setUsers(users);
console.log(checkedUsers);
};
return (
<div className="list-container">
<div className="search">
<input
type="text"
placeholder="Search for users"
onChange={(event) => setSearch(event.target.value)}
/>
</div>
{filteredUsers &&
filteredUsers.map((user) => (
<div
className="user-block"
key={user.id}
onClick={() => console.log(user.id, user.active)}>
<div className="details">
<h5>
{user.first_name} {user.last_name}
</h5>
<p>{user.email}</p>
</div>
<input
type="checkbox"
checked={user.active}
onClick={(event) => changeChecked(user.id)}
/>
</div>
))}
</div>
);
}
export default UserList;
A few things here:
I think you should map the user after the fetch to add the active with a default value, so it isn't undefined in any case:
useEffect(() => {
const fetchUsers = async () => {
try {
const request = await fetch(users);
const response = request.map(data => ({...data, active: true})).sort(function (a, b) {
if (a.last_name.toLowerCase() < b.last_name.toLowerCase()) return -1;
if (a.last_name.toLowerCase() > b.last_name.toLowerCase()) return 1;
else return;
});
setUsers(response);
} catch (error) {
console.log(error);
}
};
fetchUsers();
}, []);
Your filtered users is absolutely useless, you can do that with the user itself and lose the useEffect and the state variables entirely:
users.filter(user =>
user.first_name.toLowerCase().includes(search.toLowerCase()) ||
user.last_name.toLowerCase().includes(search.toLowerCase())
).map((user) => (
<div
className="user-block"
key={user.id}
onClick={() => console.log(user.id, user.active)}>
<div className="details">
<h5>
{user.first_name} {user.last_name}
</h5>
<p>{user.email}</p>
</div>
<input
type="checkbox"
checked={user.active}
onClick={(event) => changeChecked(user.id)}
/>
</div>
)
And last but not least, your changeChecked function is mutating the array. I would use .map as well instead of forEach:
const changeChecked = (id) => {
setUsers(
users.map(user => {
if (user.id === id) {
return {
...user,
active: !user.active
};
} else {
return user;
}
})
);
console.log(checkedUsers);
};

ReactJS: Manage multiple checkbox inputs with useState

I have the following example component that uses multiple checkboxes for choosing what items to remove from a list of objects:
import React, { useState } from "react";
import "./styles.css";
const data = [
{
name: "test1",
result: "pass"
},
{
name: "test2",
result: "pass"
},
{
name: "test3",
result: "pass"
},
{
name: "test4",
result: "pass"
},
{
name: "test5",
result: "pass"
}
];
export default function App() {
const [allChecked, setAllChecked] = useState(false);
const [isChecked, setIsChecked] = useState({});
const [formData, setFormData] = useState(data);
const handleAllCheck = e => {
setAllChecked(e.target.checked);
};
const handleSingleCheck = e => {
setIsChecked({ ...isChecked, [e.target.name]: e.target.checked });
};
const onDelete = () => {
console.log(isChecked);
const newData = data.filter(
item => !Object.keys(isChecked).includes(item.name)
);
console.log(newData);
setFormData(newData);
};
return (
<div className="App">
<div>
<label>All</label>
<input
name="checkall"
type="checkbox"
checked={allChecked}
onChange={handleAllCheck}
/>
<label />
</div>
{formData.map((test, index) => (
<div key={index}>
<label>{test.name}</label>
<input
type="checkbox"
name={test.name}
checked={allChecked ? true : isChecked[test.name]}
onChange={handleSingleCheck}
/>
</div>
))}
<button onClick={() => onDelete()}>DELETE</button>
</div>
);
}
This is mostly working, except for check all. It seems onChange will not update while using useState. I need to be able to select all the objects or uncheck some to mark for deletion.
Any help is greatly appreciated.
CodeSandbox Example: https://codesandbox.io/s/modest-hodgkin-kryco
UPDATE:
Okay, after some help from Richard Matsen,
Here is a new solution without direct DOM manipulation:
import React, { useState, useEffect } from "react";
import "./styles.css";
const data = [
{
name: "test1",
result: "pass"
},
{
name: "test2",
result: "pass"
},
{
name: "test3",
result: "pass"
},
{
name: "test4",
result: "pass"
},
{
name: "test5",
result: "pass"
}
];
export default function App() {
const [allChecked, setAllChecked] = useState(false);
const [isChecked, setIsChecked] = useState();
const [loading, setLoading] = useState(true);
const [formData, setFormData] = useState(data);
const handleAllCheck = e => {
setAllChecked(e.target.checked);
};
const handleSingleCheck = e => {
setIsChecked({ ...isChecked, [e.target.name]: e.target.checked });
};
const onDelete = () => {
const itemList = Object.keys(isChecked).map((key:any) => {
if (isChecked[key] === true) {
return key
}
})
const result = formData.filter((item:any) => !itemList.includes(item.name))
console.log(result)
setFormData(result)
}
useEffect(() => {
if (!loading) {
setIsChecked(current => {
const nextIsChecked = {}
Object.keys(current).forEach(key => {
nextIsChecked[key] = allChecked;
})
return nextIsChecked;
});
}
}, [allChecked, loading]);
useEffect(() => {
const initialIsChecked = data.reduce((acc,d) => {
acc[d.name] = false;
return acc;
}, {})
setIsChecked(initialIsChecked)
setLoading(false)
}, [loading])
return (
<div className="App">
<div>
<label>All</label>
<input
name="checkall"
type="checkbox"
checked={allChecked}
onChange={handleAllCheck}
/>
<label />
</div>
{!loading ? formData.map((test, index) => (
<div key={index}>
<label>{test.name}</label>
<input
type="checkbox"
name={test.name}
checked={isChecked[test.name]}
onChange={handleSingleCheck}
/>
</div>
)): null}
<button onClick={() => onDelete()}>DELETE</button>
</div>
);
}
codesandbox of working solution:
https://codesandbox.io/s/happy-rubin-5zfv3
The basic problem is checked={allChecked ? true : isChecked[test.name]} stops the unchecking action from happening - once allChecked is true it does not matter what value isChecked[test.name] has, the expression is always going to be true.
You should rely only on isChecked for the value, and treat changing allChecked as a side-effect.
useEffect(() => {
setIsChecked(current => {
const nextIsChecked = {}
Object.keys(current).forEach(key => {
nextIsChecked[key] = allChecked;
})
return nextIsChecked;
});
}, [allChecked]);
...
{formData.map((test, index) => (
<div key={index}>
<label>{test.name}</label>
<input
type="checkbox"
name={test.name}
checked={isChecked[test.name]}
onChange={handleSingleCheck}
/>
</div>
))}
There's also this warning cropping up
Warning: A component is changing an uncontrolled input of type checkbox to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.
So that's basically saying don't initialize isChecked to {}, because the input's checked property is initially undefined. Use this instead,
{
test1: false,
test2: false,
test3: false,
test4: false,
test5: false,
}
or this way
const data = { ... }
const initialIsChecked = data.reduce((acc,d) => {
acc[d.name] = false;
return acc;
}, {})
export default function App() {
const [allChecked, setAllChecked] = useState(false);
const [isChecked, setIsChecked] = useState(initialIsChecked);
...
The problem with your code was how you were handling allChecked. I have made some changes to your code and it works now.
const data = [
{
name: "test1",
result: "pass"
},
{
name: "test2",
result: "pass"
},
{
name: "test3",
result: "pass"
},
{
name: "test4",
result: "pass"
},
{
name: "test5",
result: "pass"
}
];
function App() {
const [allChecked, setAllChecked] = useState(false);
// using an array to store the checked items
const [isChecked, setIsChecked] = useState([]);
const [formData, setFormData] = useState(data);
const handleAllCheck = e => {
if (allChecked) {
setAllChecked(false);
return setIsChecked([]);
}
setAllChecked(true);
return setIsChecked(formData.map(data => data.name));
};
const handleSingleCheck = e => {
const {name} = e.target;
if (isChecked.includes(name)) {
setIsChecked(isChecked.filter(checked_name => checked_name !== name));
return setAllChecked(false);
}
isChecked.push(name);
setIsChecked([...isChecked]);
setAllChecked(isChecked.length === formData.length)
};
const onDelete = () => {
const data_copy = [...formData];
isChecked.forEach( (checkedItem) => {
let index = formData.findIndex(d => d.name === checkedItem)
delete data_copy[index]
}
)
setIsChecked([])
// filtering out the empty elements from the array
setFormData(data_copy.filter(item => item));
setAllChecked(isChecked.length && isChecked.length === data.length);
};
return (
<div className="App">
<form>
<label>All</label>
<input
name="checkall"
type="checkbox"
checked={allChecked}
onChange={handleAllCheck}
/>
{ formData.map((test, index) => (
<div
key={index}
>
<label>{test.name}</label>
<input
type="checkbox"
name={test.name}
checked={isChecked.includes(test.name)}
onChange={handleSingleCheck}
/>
</div>
))
}
<label />
</form>
<button onClick={onDelete}>DELETE</button>
</div>
);
}
I think you should merge allChecked and isChecked state vars, because they represent the same thing, but your denormalizing it by creating two different vars! I suggest to keep isChecked, and modify all its entries when you press the allChecked input. Then, you can use a derived var allChecked (defined in your component or by using useMemo hook) to know if all your checks are checked or not.
Well, after some time working I came up with:
import React, { useState } from "react";
import "./styles.css";
import { useFormInputs } from "./checkHooks";
const data = [
{
name: "test1",
result: "pass"
},
{
name: "test2",
result: "pass"
},
{
name: "test3",
result: "pass"
},
{
name: "test4",
result: "pass"
},
{
name: "test5",
result: "pass"
}
];
export default function App() {
const [fields, handleFieldChange] = useFormInputs({
checkedAll: false
});
const allcheck = () => {
const checkdata = document.querySelectorAll(".checkers").length;
const numChecks = Array.from(new Array(checkdata), (x, i) => i);
numChecks.map(item => {
console.log(item);
async function checkThem() {
let element = await document.getElementsByClassName("checkers")[item];
element.click();
}
return checkThem();
});
};
return (
<div className="App">
<div>
<label>All</label>
<input name="checkall" type="checkbox" onChange={allcheck} />
<label />
</div>
{data.map((test, index) => (
<div key={index}>
<label>{test.name}</label>
<input
className="checkers"
type="checkbox"
name={test.name}
onChange={handleFieldChange}
/>
</div>
))}
</div>
);
}
Relevent codesandbox: https://codesandbox.io/s/admiring-waterfall-0vupo
Any suggestions welcomed. Also, thanks for the help guys!

Resources