How to handle multiple checkboxes in React form? - reactjs

I have a form that loads preset checkbox selections from the backend. I query the checkbox menu and iterate over it in JSX. Now I want to be able to select/deselect. To handle this, I start out by creating an array of objects matching the size of menu checkbox items like so:
useEffect(() => {
if (!profile) { // this checks if the reusable form is a create or update. If this is an update, I need to prefill an existing array of objects with added key/value pair of checked: true
setConcerns(Array(menuItems.length).fill({ checked: false }))
}
}, [menuItems]) // this happens first
// if it's an update form
useEffect(() => {
if (profile) {
const updatedConcerns = existingConcerns.map((c, i) => ({
...concerns,
c.id: c.id,
checked: true,
}))
}
}, [existingConcerns]) // this happens second
This sets up the concerns array for toggle.
In my JSX I have a Checkbox component:
{menuItems &&
menuItems.map((item: CheckboxProps, index: number) => (
<Checkbox
testID={item?.title}
label={item?.title}
checked={concerns && !!concerns[index]?.checked}
onValueChange={() => handleConcerns(index)}
/>
))}
And the handleConcerns method so far:
const handleConcerns = (index: number) => {
const concernsCopy = [...concerns]
concernsCopy[index].checked = !concernsCopy[index].checked
setConcerns(concernsCopy)
// the rest will determine how to add selected items to concerns
array for submit
}
This causes all of the checkboxes to toggle. This is just the start of pushing menu data into each index of concerns array but first need to get the checkboxes working and be able to have an array of chosen indices to submit to backend.

Related

Deselect checkboxes based on dropdown select value in ProComponents React

Resources for ProComponents that are being used in this demo.
ProForm same properties as ProModal
ProFormCheckBox.Group
ProFormSelect
I am trying to deselect all checkboxes every time I select a new item.
So for example if I select PCA from the dropdown and checked boxed 2.
Example 1
Then switched over to LSCA from the dropdown I want to deselect all checkboxes.
Example 2
Instead, what happens check box 2 is still selected.
Example 3
I have it set where each dropdown item has a different list of checkboxes set into it.
They are four different arrays. The more interesting parts are the three different useState. One controls the state of which dropdown item is selected. Another controls the state of which array of checkboxes should be displayed. The last controls the state of which checkboxes should be marked. Notes of interest in the code are
// Controls the state of the dropdown menu to be selected
const [selected, setSelected] = useState('');
// Controls the state of which array of checkboxes should be displayed
const [checkBoxes, setCheckBoxes] = useState([]);
// Controls the state of which checkboxes are checkmarked
const [markedCheckBoxes, setMarkedCheckBoxes] = useState([]);
Next code of interest is the function changeSelectOptionHandler which is run onChange of the ProFormSelect. This should run setMarkedCheckBoxes to set the state into an empty array so no boxes are selected.
/** Function that will set different values to state variable
* based on which dropdown is selected
*/
const changeSelectOptionHandler = (event) => {
// This should set the state of the setMarkedCheckBoxes to be empty
setMarkedCheckBoxes([]);
// Sets the state of which array of checkboxes should be displayed based on event
checkBoxOptions(event);
// Sets the state of which dropdown is selected based on the event
setSelected(event);
};
According to the docs I should set the value as what checkBoxes should be marked in ProFormCheckbox.Group
<ProFormCheckbox.Group
name="rows"
label="Select Rows"
options={checkBoxes}
onChange={(e) => {
console.log('state changes');
setMarkedCheckBoxes(e);
}}
// This is where I set which checkboxes should be marked with value
// initialValue={markedCheckBoxes}
value={markedCheckBoxes}
/>
I was able to use the React Dev Tools and confirm values are updated for markedCheckBoxes based on when a new dropdown item is selected which should be an empty array. I also tested that when I cancel or submit the modalForm that markedCheckBoxes is an empty array and is correctly displayed with setting the value on the ProFormCheckbox.Group. So I am stumped on how to correctly display what is on the value in ProFormCheckbox.Group after updating the select menu. Below is the full code snippet of said RowModal component.
import { PlusOutlined } from '#ant-design/icons';
import { Button, message } from 'antd';
import { useState } from 'react';
import ProForm, { ModalForm, ProFormSelect, ProFormCheckbox } from '#ant-design/pro-form';
import { updateRule } from '#/services/ant-design-pro/api';
const RowModal = ({ orderId, actionRef }) => {
/** Different arrays for different dropdowns */
const eca = ['1', '2', '3', '4', '5'];
const pca = ['1', '2'];
const lsca = ['1', '2', '3', '4', '5', '6'];
const mobility = ['1', '2', '3', '4'];
// Controls the state of the dropdown menu to be selected
const [selected, setSelected] = useState('');
// Controls the state of which array of checkboxes should be displayed
const [checkBoxes, setCheckBoxes] = useState([]);
// Controls the state of which checkboxes are checkmarked
const [markedCheckBoxes, setMarkedCheckBoxes] = useState([]);
/** Function that will set different values to state variable
* based on which dropdown is selected
*/
const changeSelectOptionHandler = (event) => {
// This should set the state of the setMarkedCheckBoxes to be empty
setMarkedCheckBoxes([]);
// Sets the state of which array of checkboxes should be displayed based on event
checkBoxOptions(event);
// Sets the state of which dropdown is selected based on the event
setSelected(event);
};
/** This will be used to create set of checkboxes that user will see based on what they select in dropdown*/
const checkBoxOptions = (event) => {
/** Setting Type variable according to dropdown */
if (event === 'ECA') setCheckBoxes(eca);
else if (event === 'PCA') setCheckBoxes(pca);
else if (event === 'LSCA') setCheckBoxes(lsca);
else if (event === 'Mobility') setCheckBoxes(mobility);
else setCheckBoxes([]);
};
return (
<ModalForm
title="Assign to Area and Row"
trigger={
<Button type="primary">
<PlusOutlined />
Assign
</Button>
}
autoFocusFirstInput
modalProps={{
destroyOnClose: true,
onCancel: () => {
setSelected('');
setCheckBoxes([]);
setMarkedCheckBoxes([]);
},
}}
onFinish={async (values) => {
const newValues = { ...values, order: orderId };
const req = await updateRule('http://127.0.0.1:3000/api/v1/floorPlans', {
data: newValues,
});
message.success('Success');
setSelected('');
setCheckBoxes([]);
setMarkedCheckBoxes([]);
actionRef.current?.reloadAndRest?.();
return true;
}}
// initialValues={{ rows: ['A'] }}
>
<ProForm.Group>
<ProFormSelect
request={async () => [
{
value: 'PCA',
label: 'PCA',
},
{
value: 'ECA',
label: 'ECA',
},
{
value: 'LSCA',
label: 'LSCA',
},
{
value: 'Mobility',
label: 'Mobility',
},
]}
// On change of dropdown, changeSelectOptionHandler will be called
onChange={changeSelectOptionHandler}
width="xs"
name="area"
label="Select Area"
value={selected}
/>
</ProForm.Group>
<ProFormCheckbox.Group
name="rows"
label="Select Rows"
options={checkBoxes}
onChange={(e) => {
console.log('state changes');
setMarkedCheckBoxes(e);
}}
// This is where I set which checkboxes should be marked with value
// initialValue={markedCheckBoxes}
value={markedCheckBoxes}
/>
</ModalForm>
);
};
export default RowModal;
Thanks in advance!
ProForm is a repackaging of antd Form
So you can use Form API for your purpose
import { useForm } from 'antd/lib/form/Form'
//...
const [form] = useForm();
// and then pass FormInstance in your component
<ModalForm
form={form}
//...
/>
// then in your handlers where you want to modify values use
form.setFieldsValue({
"fieldName": value
})

Push a number in useState array

In a form having multiple checkbox, I want to store the values of the check box in an useState array after clicking submit. Also the user may check/uncheck a check box multiple times before submitting the form. What can be the approach/code?
Every checkbox have a "checked" property, so in state you must have an array with all the checked checkboxes.
const [selectedCheckboxes, setSelectedCheckboxes] = useState([]);
For example you might store also the checkboxes on an array:
const checkboxes = [{name: 'cb1', label:'cb1'}, {name: 'cb2', label:'cb3'}, ...];
and all of them should have the same onChange method:
onCheckBoxChange = (event) => {
const selectedCheckboxes = [...selectedCheckboxes];
const isChecked = selectedCheckboxes.includes(event.target.name);
if (!isChecked) {
selectedCheckboxes.push(event.target.name);
} else {
selectedCheckboxes.splice(selectedCheckboxes.indexOf(event.target.name), 1);
}
setSelectedCheckboxes(selectedCheckboxes);
};
a common function that verify if a checkbox is checked:
isChecked = (cb) => selectedCheckboxes.includes(cb.name);
and every checkbox should look like:
<CheckBox
name={cb.name}
checked={isChecked(cb)}
onChange={onCheckBoxChange}
{...props}
/>
You can make it multiple ways. You can just make const checkboxOpenArray = [] where you will just store boolean values like [true, false, false, true] so later in the code where you probably .map through all checkboxes you just check if checkboxOpenArray[idx] where idx you get from .map. If it is true you just set checkbox to true. Same for onChange - you just find idx in the checkboxOpenArray and set it to false or true.

Populating a Material-UI dropdown with Redux store data

I am trying to get my Redux store fields to automatically populate a method I have imported. Am I going about this the right way in order to get this done? Do I need to create a mapping options for each field?
I have each of my dropdowns inserted with a PopulateDropdown list and the fields in each of them but I need them split as per the id and text.
Am I accessing my redux store correctly below? I have the array declared on up my function component by using const fields = useSelector(state => state.fields);
Update
I have the method inserted into where the dropdowns should be however I don't think I am accessing the data correctly which is causing the problem. The fields array has been de-structured into the six different fields for each dropdown and different mappingOptions have been created for each one.
What do I need to do to get the data into the method? the examples I have seen have static arrays declared on the component rather than use the Redux store.
const fields = useSelector(state => state.fields);
// can destructure individual fields
const { diveSchoolList, currentList, regionList, diveTypeList, visibilityList, diveSpotList } = fields;
populateDropdown method that I have imported
export const PopulateDropdown = ({ dataList = [], mappingOptions, name, label }) => {
const { title, value } = mappingOptions;
return (
<FormControl style={{ width: 200 }} >
<InputLabel id={label}>{label}</InputLabel>
<Select labelId={label} name={name} >
{dataList.map((item) => (
<MenuItem value={item[value]}>{item[title]}</MenuItem>
))}
</Select>
</FormControl>
);
};
imported dropdown menu
<PopulateDropdown
dataList={diveType}
mappingOptions={mappingOptions}
name="fieldName"
label="Select dive type"
value={dive.typeID}
onChange={handleChange}/>
Update
I have updated my action, reducer and populateFields method however I am still having trouble mapping the redux data to my two property fields. In the Redux tree the fields should be under the fields.data.fieldlists as they print when I console log them.
What way should I be populating them into the titleProperty etc? It is currently looking like it might be populating but a large box drops downs that I can't see any values inside.
// select user object from redux
const user = useSelector(state => state.user);
// get the object with all the fields
const fields = useSelector(state => state.fields);
// can destructure individual fields
const { diveSchoolList = [],
currentList = [],
regionList = [],
diveTypeList = [],
visibilityList = [],
diveSpotList = [],
marineTypeList = [],
articleTypeList = []
} = fields;
.........
<PopulateDropdown
dataList={fields.data.diveTypeList} // the options array
titleProperty={fields.data.diveTypeList.diveTypeID} // option label property
valueProperty={fields.data.diveTypeList.diveType} // option value property
label="Dive Type Name" // label above the select
placeholder="Select dive type" // text show when empty
value={dive.typeID} // get value from state
onChange={handleChange(setDive.typeID)} // update state on change
/>
Your PopulateDropdown component looks correct except that we need it to use the value and onChange that we passed down as props.
My personal preference would be to use separate properties valueProperty and titleProperty instead of passing a single mappingOptions. That way you don't need to create objects for every dropdown, you just set the two properties in your JSX. You could get rid of this part entirely if you normalized your data such that the elements of every list have the same properties id and label.
<PopulateDropdown
dataList={diveTypeList} // the options array
titleProperty={"diveTypeId"} // option label property
valueProperty={"diveType"} // option value property
label="Dive Type Name" // label above the select
placeholder="Select dive type" // text show when empty
value={dive.typeID} // get value from state
onChange={handleChange("typeId")} // update state on change
/>
export const PopulateDropdown = ({
dataList = [],
valueProperty,
titleProperty,
label,
...rest // can just pass through all other props to the Select
}: Props) => {
return (
<FormControl style={{ width: 200 }}>
<InputLabel id={label}>{label}</InputLabel>
<Select {...rest} labelId={label}>
{dataList.map((item) => (
<MenuItem value={item[valueProperty]}>{item[titleProperty]}</MenuItem>
))}
</Select>
</FormControl>
);
};
It looks like the ids in currentId are actually a number, so at some point in your code you will want to convert that with parseInt because e.target.value is always a string, though maybe the backend can handle that.
Loading the API Data
It looks like you figured out how to fetch all of the fields in one API call which is great. You are saving it to a property fields on the fields reducer which creates the structure state.fields.fields. Since you are replacing the whole state, you can just return the whole thing as the entire slice state.
You can initialize your state object with empty arrays, or you can use an empty object {} as your initial state and fallback to an empty array when you destructure the arrays off of it, like const {diveSchoolList = [], currentList = []} = fields.
export const requireFieldData = createAsyncThunk(
"fields/requireData", // action name
// don't need any argument because we are now fetching all fields
async () => {
const response = await diveLogFields();
return response.data;
},
// only fetch when needed: https://redux-toolkit.js.org/api/createAsyncThunk#canceling-before-execution
{
// _ denotes variables that aren't used - the first argument is the args of the action creator
condition: (_, { getState }) => {
const { fields } = getState(); // returns redux state
// check if there is already data by looking at the didLoadData property
if (fields.didLoadData) {
// return false to cancel execution
return false;
}
}
}
);
const fieldsSlice = createSlice({
name: "fields",
initialState: {
currentList: [],
regionList: [],
diveTypeList: [],
visibilityList: [],
diveSpotList: [],
diveSchoolList: [],
marineTypeList: [],
articleTypeList: [],
didLoadData: false,
},
reducers: {},
extraReducers: {
// picks up the pending action from the thunk
[requireFieldData.pending.type]: (state) => {
// set didLoadData to prevent unnecessary re-fetching
state.didLoadData = true;
},
// picks up the success action from the thunk
[requireFieldData.fulfilled.type]: (state, action) => {
// want to replace all lists, there are multiple ways to do this
// I am returning a new state which overrides any properties
return {
...state,
...action.payload
}
}
}
});
So in the component we now only need to call one action instead of looping through the fields.
useEffect(() => {
dispatch(requireFieldData());
}, []);

ReactJS sort by Asc/Desc not rendering sorted list from nested component

Project Overview
I am currently learning ReactJS and am creating a Pokedex app that allows the user to sort Pokemon objects by specific properties (name, type, hp, etc). The end product should have a search input to filter by pokemon name, a drop-down selector for sort criteria, and buttons for sort Ascend and sort Descend. State being tracked is searchQuery, sortBy, sortSelected, and pokeData(my data file).
Full code can be viewed here: https://github.com/julianne-vela/Pokedex-React/tree/dev
File Structure: SearchPage.js > SideBar.js > SortMenu.js
Problem
onClick button in SortMenu is not rendering sorted Pokemon list.
Expected Result: Click Asc/Desc button in SortMenu -> Pokemon list renders in sorted order based on criteria selected in drop-down and Asc/Desc button clicked.
Actual Result: Drop-down is updating state with correct sort criteria selected but button is not triggering any action when clicked.
What I've tried
Using destructured props throughout project
const {
sortBy,
sortSelected,
} = this.state
Currently passing the following props from SearchPage.js to child SideBar.js:
sortByValues={sortBy} // Array of sort criteria options stored in state
sortSelected={sortSelected} // Currently selected sort criteria from drop-down
handleSortSelected={this.handleSortSelected} // event handler to update state with currently selected sort criteria
sortAsc={this.sortAsc} // function to trigger Asc sort onClick
sortDesc={this.sortDesc} // function to trigger Asc sort onClick
SortMenu.js Code
import React, { Component } from 'react';
export default class SortMenu extends Component {
render() {
const {
sortByValues,
handleSortSelected,
sortAsc,
sortDesc,
} = this.props
const options = sortByValues.map(option => <option value={option} key={option}>{option}</option>)
return (
<aside>
{/* DropDown Sort By */}
<select className='dropdown'
onChange={handleSortSelected}>
{options}
</select>
{/* Sort Ascending/Descending Buttons */}
<button className='sortBtn' value='ascending' onClick={sortAsc}>Ascending</button>
<button className='sortBtn' value='descending' onClick={sortDesc}>Descending</button>
</aside >
)
}
}
I feel it's important to note that we are using strictly class components and therefore are not using constructor(props) in this project. Instead, I am using arrow functions throughout to implicitly bind this where needed.
To reiterate my goal: I need to dynamically render the pokemon list based on the sort criteria selected in the drop-down list as well as the ascend/descend button clicked. The list should update onClick.
I have been working on this for 3 days and have gone through so many iterations of sort functions that I can't even see straight anymore. I'm thinking this might be a simple over-sight due to the sheer amount of code I'm writing as well as the fact that this is a new language (I'm already proficient in Vanilla JS).
Any guidance here would be GREATLY appreciated as I'm really at a wall at this point. I don't know what else to try in order to render the sorted list.
Update
Also, would it be easier if I were to create a toggle button instead of two separate buttons for Asc/Desc?
Thank you!
Update 2
Currently I am rendering my pokemon objects in a module component that I'm calling within the SearchPage.js component. Here is the code for the Pokemon List:
export default class PokemonList extends Component {
render() {
const { filteredPokemon } = this.props
return (
<content className='pokemon-list float'>
{filteredPokemon.map(pokeObject =>
<PokeItem
key={pokeObject._id}
pokeImage={pokeObject.url_image}
pokeName={capFirstLetter(pokeObject.pokemon)}
pokeType={capFirstLetter(pokeObject.type_1)}
pokeHp={pokeObject.hp}
pokeAtt={pokeObject.attack}
pokeDef={pokeObject.defense}
/>)}
</content>
);
}
}
This is pulling the filtered pokemon from my filter method on SearchPage.js:
const filteredList = pokeData.filter(pokeObject => {
return pokeObject['pokemon'].includes(this.state.searchQuery) || pokeObject['type_1'].includes(tFilterSelected);
});
To sort the items, I'm using two separate arrow functions. These are housed in the first section of the SearchPage.js component (outside of render and return):
sortAsc = () => {
this.setState(prevState => {
this.state.pokeData.sort((a, b) => (a[this.state.sortSelected] - b[this.state.sortSelected]))
})
}
sortDesc = () => {
this.setState(prevState => {
this.state.pokeData.sort((a, b) => (b[this.state.sortSelected] - a[this.state.sortSelected]))
})
}
Update 3
Added project to CodeSandbox. Can view here: https://codesandbox.io/s/pokedex-react-crflb
A comparator function that subtracts the arguments would work fine for sorting numbers but would fail on strings since it would return NaN.
sortAsc = () => {
const { pokeData, sortSelected } = this.state
this.setState({
pokeData: [...pokeData].sort((a, b) => {
if (a[sortSelected] > b[sortSelected]) {
return 1
}
if (a[sortSelected] < b[sortSelected]) {
return -1
}
return 0
})
})
}
sortDesc = () => {
const { pokeData, sortSelected } = this.state
this.setState({
pokeData: [...pokeData].sort((a, b) => {
if (a[sortSelected] > b[sortSelected]) {
return -1
}
if (a[sortSelected] < b[sortSelected]) {
return 1
}
return 0
})
})
}
Working CodeSandbox

Customize Array State in ReactJs with ES6

i am fetching some data (in array) from api and assigning it to state called items. then calling an Item component for each item of the state by following code:
<div>
{items.length} results found for {search_term}
{items.map((item) =>
(item.active_price > 0 && item.show) ? <Item key={item.id} details={item} items={items} setItems={setItems}/> : ""
)}
</div>
The array looks like this:
Next plan is adding a remove button in Item component, which will change value of show to false (for the item clicked) in the items state. What I am thinking is, as the state will change, the items state will be re-rendered and the item turned to false will be hidden from view. I am adding a onCLick event listener on the button and the function is:
const handleClick = () => {
console.log(({...props.items,[props.details.number - 1]:false}))
// props.setItems(prevState=>({...prevState,[props.details.number - 1]:false}))
}
I'll call props.setItems later but for now, I am trying to see if I can just edit that element to turn the show into false. the code above replace the whole index with false.
in the onClick function, I only want to edit the value of show, not replace the entire entry. I have tried:
({...props.items,[props.details.number - 1][show]:false})
and
({...props.items,[props.details.number - 1].show:false})
It shows syntax error before [show] and .show respectively. How can I edit my handleClick function to edit the value of show properly.
Thanks.
It looks like you are converting your array into an object in your current method. (the first screen shot is an array, the second is an object). You're going to run into issues there.
To just remove the item from the array, it would be easiest just to use .filter();
const handleClick = () => {
props.setItems(
prevState => prevState.filter(item => item.number !== props.details.number)
);
}
To set the show property is a bit more complicated. This is pretty standard way to do so without mutating.
const handleClick = () => {
props.setItems(prevState => {
const itemIndex = prevState.findIndex(
item => item.number == props.details.number
);
// Using 'props.details.number - 1' will not be a valid index when removing items
return [
...prevState.slice(0, itemIndex), // Your state before the item being edited
{...prevState[itemIndex], show: false}, // The edited item
...prevState.slice(itemIndex + 1) // The rest of the state after the item
];
});
}
You can try the following syntax of code, assuming items is state here-
const [items,setItems]=.....
The following is the code
const handleClick = () => {
setItems({
...items,
show: false,
})}
It will update the items state with value show as false.

Resources