I am mapping multiple radio buttons (group) options and when the user click on radio buttons, would like to add selected values and uniqueIds to an array.
with my current code I can get the value that I am currently clicking on but can't add to array.
{result !== null && result.length > 0 ? (
<div>
{result.map(item => {
const label = item.question
const listOptions = item.options.map(item => {
return (
<Radio
name={item.uniqueId}
inline
value={item.uniqueId}
key={item.uniqueId}
className="radio-options"
checked={item.checked}
onChange={e => {
this.handleChange(label, uniqueId);
}}
>
{item.options}
</Radio>
);
});
return (
<div className="radio-options-overview">
<p>{label}</p>
{listOptions}
</div>
);
})}
handleChange = (label, uniqueId) => {
console.log(label, uniqueId);
let addToArray = [];
this.setState({ selectedItems: addToArray})
};
array would look something like this,
[
{ "label": "ageRange", "uniquId": 2 },
{ "label": "smoker", "uniquId": 1 },
{ "label": "exOption", "uniquId": 3 }
]
You are nearly there. #Clarity provided good solution.
if you wanting to replace exisiting value and replace it with new one
Try This
handleChange = (label, uniqueId) => {
const { selectedItems } = this.state
// Find items that already exists in array
const findExistingItem = selectedItems.find((item) => {
return item.uniqueId === uniqueId;
})
if(findExistingItem) {
// Remove found Item
selectedItems.splice(findExistingItem);
// set the state with new values/object
this.setState(state => ({
selectedItems: [...state.selectedItems, {
label, uniqueId
}]
}))
} else {
// if new Item is being added
this.setState(state => ({
selectedItems: [...state.selectedItems, {
label, uniqueId
}]
}))
}
};
You can do like this:
handleChange = (label, uniqueId) => {
this.setState(state => ({ selectedItems: [...state.selectedItems, uniqueId]}));
};
By using array spread and functional form of setState you make sure that you don't directly mutate the state and add the items to the latest state.
In case you'd want to add an object with a label: uniqueId pair, you could do like so:
handleChange = (label, uniqueId) => {
this.setState(state => ({
selectedItems: [...state.selectedItems, {
[label]: uniqueId
}]
}));
};
EDIT: If you want to overwrite the items with the same labels, the easiest would be to store them as an object and not an array, so the item with the same label would overwrite an existing one:
handleChange = (label, uniqueId) => {
this.setState(state => {
return { selectedItems: { ...state.selectedItems, [label]: uniqueId } };
});
};
Honestly, I don't understand, what are you trying to do, but if you need to add object (or something else) inside an array, you could use .push() method. For example:
let addToArray = [];
let test = {"label": "ageRange", "uniquId": 2};
addToArray.push(test);
console.log(addToArray); //[{label: "ageRange", uniquId: 2}]
Related
I have a filter panel where the user can select different color filters:
// ColorButton.jsx
function ColorButton({ color }) {
const handleFilterSelected = ({ id }) => {
dispatch(applyFilter({ type: "colors", value: id }));
};
return (
<div className="color-button" onClick={() => handleFilterSelected(color)}>
{isFilterSelected({ type: "colors", value: color.id }) ? "yay" : "nay"}
</div>
);
}
The selected filters are stored in the redux store and the isFilterSelect function looks like this:
// redux/utils/filter.utils.js
export const isFilterSelected = ({ type, value }) => {
const {
filters: { applied }
} = store.getState();
return applied
.filter((f) => f.type === type)
.map((f) => f.value)
.includes(value);
};
The issue is that the check runs before a selected filter is added to the applied array.
As a more general question - is it even a good idea to have "helper" functions that depend on the redux store?
Your helper function there should be written as a selector that gets the current state, and you should be using useSelector instead of store.getState manually, as that will update your component when the selector value changes.
function ColorButton({ color }) {
const isSelected = useSelector(state => isFilterSelected(state, { type: "colors", value: color.id }));
return (
<div className="color-button">
{isSelected ? "yay" : "nay"}
</div>
);
}
// redux/utils/filter.utils.js
export const isFilterSelected = (state, { type, value }) => {
return state.filters.applied
.filter((f) => f.type === type)
.map((f) => f.value)
.includes(value);
};
My code is basically a form with a text input and a submit button. Each time the user input data, my code adds it to an array and shows it under the form.
It is working fine; however, when I add duplicate values, it still adds it to the list. I want my code to count these duplicates and show them next to each input.
For example, if I input two "Hello" and one "Hi" I want my result to be like this:
2 Hello
1 Hi
Here is my code
import React from 'react';
import ShoppingItem from './ShoppingItem';
class ShoppingList extends React.Component {
constructor (props){
super(props);
this.state ={
shoppingCart: [],
newItem :'',
counter: 0 };
}
handleChange =(e) =>
{
this.setState ({newItem: e.target.value });
}
handleSubmit = (e) =>
{
e.preventDefault();
let newList;
let myItem ={
name: this.state.newItem,
id:Date.now()
}
if(!this.state.shoppingCart.includes(myItem.name))
{
newList = this.state.shoppingCart.concat(myItem);
}
if (this.state.newItem !=='')
{
this.setState(
{
shoppingCart: newList
}
);
}
this.state.newItem ="" ;
}
the rest of my code is like this:
render(){
return(
<div className = "App">
<form onSubmit = {this.handleSubmit}>
<h6>Add New Item</h6>
<input type = "text" value = {this.state.newItem} onChange ={this.handleChange}/>
<button type = "submit">Add to Shopping list</button>
</form>
<ul>
{this.state.shoppingCart.map(item =>(
<ShoppingItem item={item} key={item.id} />
)
)}
</ul>
</div>
);
}
}
export default ShoppingList;
Issues
this.state.shoppingCart is an array of objects, so this.state.shoppingCart.includes(myItem.name) will always return false as it won't find a value that is a string.
this.state.newItem = ""; is a state mutation
Solution
Check the newItem state first, if empty then return early
Search this.state.shoppingCart for the index of the first matching item by name property
If found then you want to map the cart to a new array and then also copy the item into a new object reference and update the quantity.
If not found then copy the array and append a new object to the end with an initial quantity 1 property.
Update the shopping cart and newItem state.
Code
handleSubmit = (e) => {
e.preventDefault();
if (!this.state.newItem) return;
let newList;
const itemIndex = this.state.shoppingCart.findIndex(
(item) => item.name === this.state.newItem
);
if (itemIndex !== -1) {
newList = this.state.shoppingCart.map((item, index) =>
index === itemIndex
? {
...item,
quantity: item.quantity + 1
}
: item
);
} else {
newList = [
...this.state.shoppingCart,
{
name: this.state.newItem,
id: Date.now(),
quantity: 1
}
];
}
this.setState({
shoppingCart: newList,
newItem: ""
});
};
Note: Remember to use item.name and item.quantity in your ShoppingItem component.
Replace your "handleSubmit" with below one and check
handleSubmit = (e) => {
e.preventDefault();
const { shoppingCart, newItem } = this.state;
const isInCart = shoppingCart.some(({ itemName }) => itemName === newItem);
let updatedCart = [];
let numberOfSameItem = 1;
if (!isInCart && newItem) {
updatedCart = [
...shoppingCart,
{
name: `${numberOfSameItem} ${newItem}`,
id: Date.now(),
itemName: newItem,
counter: numberOfSameItem
}
];
} else if (isInCart && newItem) {
updatedCart = shoppingCart.map((item) => {
const { itemName, counter } = item;
if (itemName === newItem) {
numberOfSameItem = counter + 1;
return {
...item,
name: `${numberOfSameItem} ${itemName}`,
itemName,
counter: numberOfSameItem
};
}
return item;
});
}
this.setState({
shoppingCart: updatedCart,
newItem: ""
});
};
I have list items represent names, when clicking any name it turns red then take one second to return black again, but clicking two identical names consecutively make them keep red color, not turning black again
you can imagine it as a memory game, but i tried to make a simple example here of what i am trying to achieve in the original project
This is my code and my wrong trial:
const App = () => {
const { useState } = React;
const items = [
{
name: 'mark',
id: 1,
red: false
},
{
name: 'peter',
id: 2,
red: false
},
{
name: 'john',
id: 3,
red: false
},
{
name: 'mark',
id: 4,
red: false,
},
{
name: 'peter',
id: 5,
red: false
},
{
name: 'john',
id: 6,
red: false
}
];
const [names, setNames] = useState(items);
const [firstName, setFirstName] = useState(null);
const [secondName, setSecondName] = useState(null)
const handleItemClick = (item) => {
setNames(prev => prev.map(i => i.id === item.id ? { ...i, red: true } : i));
//the problem is here
setTimeout(() => {
setNames(prev => prev.map(n => {
if (secondName && (secondName.name === firstName.name) && n.name === firstName.name) {
return { ...n,red: true }
}
return { ...n, red: false };
}))
}, 1000)
if (!firstName) setFirstName(item);
else if (firstName && !secondName) setSecondName(item)
else if (firstName && secondName) {
setFirstName(item);
setSecondName(null)
}
}
return (
<div class="app">
<ul class="items">
{
names.map(i => {
return (
<Item
item={i}
handleItemClick={handleItemClick}
/>
)
})
}
</ul>
</div>
)
}
const Item = ({ item, ...props }) => {
const { id, name, red } = item;
const { handleItemClick } = props;
return (
<li
className={`${red ? 'red' : ''}`}
onClick={() => handleItemClick(item)}
>
{name}
</li>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
But this code doesn't work correctly, when clicking two identical names consecutively they don't keep red color and turning black again
To me it seems the issue is overloading the event handler and violating the Single Responsibility Principle.
The handler should be responsible for handling the click event and nothing else. In this case, when the element is clicked you want to add the id to the state of selected/picked names, and toggle the red state value of item with matching id. Factor the timeout effect into (strangely enough) an useEffect hook, with the picks as dependencies. This inverts the logic of the timeout to clearing/resetting the state versus setting what is "red" or not. You can/should also move any logic of determining matches into this same effect (since it already has the dependencies anyway).
useEffect(() => {
... logic to determine matches
const timer = setTimeout(() => {
// time expired, reset only if two names selected
if (firstName && secondName) {
setFirstName(null);
setSecondName(null);
setNames(names => names.map(name => ({ ...name, red: false })));
}
}, 1000);
// clean up old timeout when state updates, i.e. new selected
return () => clearTimeout(timer);
}, [firstName, secondName]);
This will allow you to simplify your name setting logic to
if (!firstName) {
setFirstName(item);
} else {
setSecondName(item);
}
Note: I believe you need another data structure to hold/track/store existing matches made by the user.
How this works:
Starting from clean state, no names are chosen
When first name is picked, firstName is null and updated, red state updated
Timeout is set (but won't clear state yet)
When second name is picked, firstName is defined, so secondName is updated, red state updated
If match, add match to state (to keep red)
Timeout expire and reset state (go back to step 1)
The following is how I'd try to simplify state a bit more, using an array of selected ids that only update if the selected id isn't already chosen and 2 picks haven't been chosen yet.
const App = () => {
const [names, setNames] = useState(items);
const [picks, setPicks] = useState([]);
const [matched, setMatched] = useState({});
/**
* On click event, add id to `picks` array, allow only two picks
*/
const onClickHandler = id => () =>
picks.length !== 2 &&
!picks.includes(id) &&
setPicks(picks => [...picks, id]);
/**
* Effect to toggle red state if id is included in current picks
*/
useEffect(() => {
setNames(names =>
names.map(name => ({
...name,
red: picks.includes(name.id)
}))
);
}, [picks]);
/**
* Effect checks for name match, if a match is found it is added to the
* `matched` array.
*/
useEffect(() => {
// matches example: { mark: 1, peter: 0, john: 0 }
const matches = names.reduce((matches, { name, red }) => {
if (!matches[name]) matches[name] = 0;
red && matches[name]++;
return matches;
}, {});
const match = Object.entries(matches).find(([_, count]) => count === 2);
if (match) {
const [matchedName] = match;
setMatched(matched => ({ ...matched, [matchedName]: matchedName }));
}
const timer = setTimeout(() => {
if (picks.length === 2) {
setPicks([]);
setNames(names => names.map(name => ({ ...name, red: false })));
}
}, 1000);
return () => clearTimeout(timer);
}, [names, picks]);
return (
<div className="App">
<ul>
{names.map(item => (
<Item
key={item.id}
item={item}
matches={matched}
onClick={onClickHandler(item.id)}
/>
))}
</ul>
</div>
);
};
const Item = ({ item, matches, ...props }) => {
const { name, red } = item;
return (
<li
className={classnames({
red: red || matches[name], // for red text color
matched: matches[name] // any other style to make matches stand out
})}
{...props}
>
{name}
</li>
);
};
I am trying to set the state of features, which is an array, based on inputs into textarea. I am having no luck setting the state of features. When I press one key, that key becomes the next index value in the state. If I hit another key, that key becomes the next index value in the state. So if I manually set my initial state of features to features: ["First", "Second", "Third"],, the result will be features: ["First", "Second", "Third", "Firstf" , "Firstd"] if I hit f and then d
The relevant State
class CreateItem extends Component {
state = {
features: [""],
};
This is how I am attempting to create textarea's based on the state
<label htmlFor="features">
Features
{this.state.features.map((feature, i) => (
<div key={i}>
<textarea
key={i}
type="features"
placeholder={`${feature}`}
name={`features.${i}`}
value={this.state.features[`${i}`]}
onChange={this.handleFeatureArrayChange}
onKeyDown={this.keyPress}
/>
</div>
))}
</label>
Here is the handleChange
handleFeatureArrayChange = e => {
const { name, type, value } = e.target;
this.setState(prevState => ({
[name]: [...prevState.features, value]
}));
};
This is how I am attempting to create new textarea's each time the user hits enter:
keyPress = e => {
if (e.keyCode == 13) {
this.setState({
features: this.state.features.concat("")
});
}
};
The problem is you're just appending to state each time a key is pressed.
handleFeatureArrayChange = e => {
const { name, type, value } = e.target;
this.setState(prevState => ({
[name]: [...prevState.features, value] // this is effectively the same as .concat(value)
}));
};
You want to update the value at the current index. Try changing the name of the input to just be key and then mapping instead
handleFeatureArrayChange = e => {
const { name, type, value } = e.target;
this.setState(prevState => ({
features: prevState.features.map((feat, key) => {
if (key == name) {
return value
} else
return feat;
}
)
}));
};
I am using React and Redux in order to manage the state of a list of checkboxes. What I have so far is working but eslint is sending me back an error:
assignment to property of function parameter
So I think I am doing something wrong.
This is the component:
import { toggleCheckboxAction, setCheckboxesToChecked } from '../actions/cancellations';
const CheckboxList = ({
columnsFilterHandler,
setCheckboxesToCheckedHandler,
checkboxes,
t,
}) => {
const onChange = (value, id, event) => {
const item = checkboxes.slice().find(idx => idx.id === id);
if (item) {
item.checked = !item.checked;
columnsFilterHandler(value, id, event.target.value);
return { checkboxes };
}
return item;
};
const setCheckboxesToTrue = () => {
const items = checkboxes.filter(checkbox => checkbox.checked === false);
items.map(item => {
if (item) {
item.checked = !item.checked; // LINE THROWING THE WARNING
setCheckboxesToCheckedHandler(item.checked);
}
return item;
});
};
return (
<>
<ToolbarTitle title="Columns" />
<ToolbarOption>
<Button kind="ghost" small onClick={setCheckboxesToTrue}>
{t('cancellations.resetDefault')}
</Button>
</ToolbarOption>
{checkboxes.map(checkbox => (
<ToolbarOption>
<Checkbox
key={checkbox.id}
id={checkbox.id}
labelText={checkbox.labelText}
value={checkbox.value}
checked={checkbox.checked}
onChange={onChange}
/>
</ToolbarOption>
))}
</>
);
};
CheckboxList.propTypes = {
columnsFilterHandler: PropTypes.func.isRequired,
setCheckboxesToCheckedHandler: PropTypes.func.isRequired,
checkboxes: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
};
const mapStateToProps = state => ({
checkboxes: state.cancellations.checkboxes,
});
export default compose(
connect(
mapStateToProps,
dispatch => ({
columnsFilterHandler: (value, isChecked, valueName) => {
dispatch(toggleCheckboxAction(value, isChecked, valueName));
},
setCheckboxesToCheckedHandler: isChecked => {
dispatch(setCheckboxesToChecked(isChecked));
},
}),
),
)(translate()(CheckboxList));
On the onChange function is where I play with the checkboxes, setting them to checked or !checked.
The issue comes from the function setCheckboxesToTrue on the line item.checked = !item.checked;
This is the action:
const setCheckboxesToChecked = isChecked => ({
type: ActionTypes.CHECKED_ALL_CHECKBOXES,
payload: { isChecked },
});
And this is the reducer to set them true or false:
[ActionTypes.TOGGLE_CHECKBOX](state, action) {
return {
...state,
checkboxes: initialState.checkboxes.map(checkbox => {
if (checkbox.id !== action.payload.id) {
return checkbox;
}
return {
...checkbox,
checked: !checkbox.checked,
};
}),
};
},
And this should be the reducer to set them all to true:
[ActionTypes.CHECKED_ALL_CHECKBOXES](state, action) {
return {
...state,
checkboxes: initialState.checkboxes.map(checkbox => {
if (checkbox.id !== action.payload.id) {
return checkbox;
}
return {
...checkbox,
checked: checkbox.checked,
};
}),
};
},
Do you guys know a better way to do what I am attempting to do or something to fix the error I am getting?
So, ESLint is complaining for exactly the reason it says: you are modifying the value of a function parameter. While this is perfectly valid JS and will work, it's highly discouraged because it makes it very easy to create bugs. Specifically:
items.map(item => {
if (item) {
item.checked = !item.checked; // HERE, you modify `item` which is the parameter passed to the map callback function
setCheckboxesToCheckedHandler(item.checked);
}
return item;
});
What you should do (and have done elsewhere in your code so you might have just forgotten to here):
items.map(item => {
const newItem = { ...item }; // Create a copy of `item` so that you don't need to modify the one coming in as the function parameter
if (item) {
newItem.checked = !newItem.checked;
setCheckboxesToCheckedHandler(newItem.checked);
}
return newItem;
});