how to set a nested object in React Hooks - javascript

I'm working on a form which contains a section where users can choose the quantity and category of a concert ticket in a dropdown. This component is dynamically created and it can have more items(quantity + category). The dropdowns of the categories are related to each other. So if you choose one category the value of the other dropdowns should be set to the same selected value. But I also need the name of each category dropdown for validation.
This is how I created the category object:
const createCategory = () => {
let cat = {}
let nestedCat = {}
for (let it in item) {
for (let bt in item[it].buyertypes) {
cat2[item[it].buyertypes[bt].id] = ''
}
cat[item[it].id] = nestedCat
nestedCat = {}
}
return cat
}
const [category, setCategory] = useState(createCategory());
This is the initial object:
{​
9871: { 1: "", 2: "", 3: "" }
​
9872: { 5: "", 6: "", 8: "" }
}
How I show and handle the ticket component
const handleQuantity = e => {
setQuantity({
...quantity,
[e.target.name]: e.target.value
});
}
const handleCategory = e => {
setCategory({
...category,
[e.target.name]: e.target.value
});
}
const ShowData = () => {
let ticketIem = []
for (let it in item) {
for (let bt in item[it].buyertypes) {
let buyerType = item[it].buyertypes[bt]
ticketIem.push({
'price': buyerType.prices, 'catId': item[it].id,
'qntId': item[it].buyertypes[bt].id
})
}
}
return (
<div>
{ticketIem.map((i, index) => (
<div key={index} >
<div>
<label>Number</label>
<select
value={quantity[i.qntId]}
onChange={handleQuantity}
name={i.qntId}
>
<option value={0}>0</option>
<option value={1}>1</option>
</select>
</div>
<div>
<label>Category</label>
<select
value={category[i.catId]}
onChange={handleCategory}
name={i.catId}
>
<option value="">Choose</option>
{categories.map((cat, index) => (
<option key={index} value={cat.id} name={i.qntId}>
{cat.description} {cat.value}
</option>
))}
</select>
</div>
</div>
))}
</div>
)
}
After selecting an option in the dropdown the category object will be set like:
{
9871: "11", 9872: "7"
}
but I expect:
{​
9871: { 1: "11", 2: "11", 3: "11" }
​
9872: { 5: "7", 6: "7", 8: "7" }
}

If you want to set all the nested properties of a specific category or quantity then you'll need to also iterate the keys of those nested properties. Use Object.keys to get an array of the nested object's keys, and reduce them into a new object with the new value set for each key.
I suggest also using a functional state update since each update depends on the existing/current state as it is shallowly copied into the next state.
const handleQuantity = (e) => {
setQuantity((quantity) => ({
...quantity,
[e.target.name]: Object.keys(quantity[e.target.name]).reduce(
(obj, key) => ({
...obj,
[key]: e.target.value
}),
{}
)
}));
};
const handleCategory = (e) => {
setCategory((category) => ({
...category,
[e.target.name]: Object.keys(category[e.target.name]).reduce(
(obj, key) => ({
...obj,
[key]: e.target.value
}),
{}
)
}));
};

Related

When I remove an item from a array, how to change the status to deleted?

When the user adds a value to an object that is inside an array the status is updated to added.
I'm trying to do the same thing when that value is deleted, to update the status to deleted.
const initialName = [
{
name: "",
status: "",
},
];
export default function changeNames(){
const [name, setName] = useState([initialName ]);
const handleAdd = () => {
setName([...name, ""]);
};
const handleItemChanged = (event, index) => {
const value = event.target.value;
const list = [...name];
list[index] = { name: value + "-" + id, status: "added" };
setName(list);
};
...
}
So when I add an input field and type the name, the array looks like this:
[{…}]
0:
name: "John-id"
status: "added"
When I remove John from the array, I want smth like this:
[{…}]
0:
name: "John-id"
status: "deleted"
This is the remove function
const handleRemoveClick = (index) => {
const list = [...name];
list.splice(index, 1);
setName(list);
};
<div>
{name.map((o, i) => {
return (
<tr key={"item-" + i}>
<td>
<div>
<input
type="text"
value={o.item}
onChange={(event) => handleItemChanged(event, i)}
placeholder="Name it"
/>
</div>
</td>
<td>
<button type="button" onClick={handleRemoveClick}>
Delete
</button>
</td>
</tr>
);
})}
<div>
<button Click={handleAddClick}>
Add Language
</button>
</div>
</div>
);
How can I make it work?
I think this might help you.thank you
import { useState } from "react";
import "./styles.css";
export default function App() {
const [Name, setName] = useState([]);
const [input, setInput] = useState({ name: "", status: "" });
const handleItemChanged = (event, index) => {
const { value } = event.target;
setInput({ name: value + "-id", status: "added" });
};
const addHandler = () => {
setName((prev) => {
return [...prev, input];
});
};
const removeHandler = (i) => {
const arr = [...Name];
arr[i].status = "deleted";
setName(arr);
};
return (
<div className="App">
<input type="text" name="name" onChange={(e) =>
handleItemChanged(e)} />
<button onClick={addHandler} style={{ margin: "1rem" }}>
Add
</button>
<div>
{Name &&
Name.map((data, i) => {
return (
<div key={i}>
<h3>
{data.name} {data.status}
<button
style={{ margin: "1rem" }}
onClick={() => removeHandler(i)}
>
Delete
</button>
</h3>
</div>
);
})}
</div>
</div>
);
}
You need to change the "status" property, because there is no way of removing item and setting its property.
const handleRemoveClick = (event, index) => {
const list = [...name];
list[index].status = "deleted";
setName(list);
};
And later while rendering you have to either perform a check in map function (not really elegant), or first filter your array and then map it:
// first, not elegant in my opinion
...
{name.map(item => item.status !== "deleted" ? <div>item.name</div> : null)}
// second approach
...
{name.filter(item => item.status !== "deleted").map(item => <div>item.name</div>)}

How to filter an array and add values to a state

I have the current state as:
const [data, setData] = useState([
{ id: 1, name: "One", isChecked: false },
{ id: 2, name: "Two", isChecked: true },
{ id: 3, name: "Three", isChecked: false }
]);
I map through the state and display the data in a div and call a onClicked function to toggle the isChecked value on click:
const clickData = index => {
const newDatas = [...data];
newDatas[index].isChecked = !newDatas[index].isChecked;
setData(newDatas);
const newSelected = [...selected];
const temp = datas.filter(isChecked==true) // incomplete code, struggling here.
const temp = datas.isChecked ?
};
I have another empty state called clicked:
const[clicked, setClicked] = setState([]). I want to add all the objected whose isChecked is true from the datas array to this array. How can I do this?
I just add checkBox & onChange event instead of using div & onClick event for your understanding
import React, { useState, useEffect } from "react";
import "./style.css";
export default function App() {
const [data, setData] = useState([
{ id: 1, name: "One", isChecked: false },
{ id: 2, name: "Two", isChecked: true },
{ id: 3, name: "Three", isChecked: false }
]);
const [clicked, setClicked] = useState([]);
const clickData = index => {
let tempData = data.map(res => {
if (res.id !== index) {
return res;
}
res.isChecked = !res.isChecked;
return res;
});
setClicked(tempData.filter(res => res.isChecked));
};
useEffect(() => {
setClicked(data.filter(res => res.isChecked));
}, []);
return (
<div>
{data.map((res, i) => (
<div key={i}>
<input
type="checkbox"
checked={res.isChecked}
key={i}
onChange={() => {
clickData(res.id);
}}
/>
<label>{res.name}</label>
</div>
))}
{clicked.map(({ name }, i) => (
<p key={i}>{name}</p>
))}
</div>
);
}
https://stackblitz.com/edit/react-y4fdzm?file=src/App.js
Supposing you're iterating through your data in a similar fashion:
{data.map((obj, index) => <div key={index} onClick={handleClick}>{obj.name}</div>}
You can add a data attribute where you assign the checked value for that element, so something like this:
{data.map((obj, index) => <div key={index} data-checked={obj.isChecked} data-index={index} onClick={handleClick}>{obj.name}</div>}
From this, you can now update your isClicked state when the handleClick function gets called, as such:
const handleClick = (event) => {
event.preventDefault()
const checked = event.target.getAttribute("data-checked")
const index = event.target.getAttribute("data-index")
// everytime one of the elements get clicked, it gets added to isClicked array state if true
If (checked) {
let tempArr = [ ...isClicked ]
tempArr[index] = checked
setClicked(tempArr)
}
}
That will let you add the items to your array one by one whenever they get clicked, but if you want all your truthy values to be added in a single click, then you simply need to write your handleClick as followed:
const handleClick = (event) => {
event.preventDefault()
// filter data objects selecting only the ones with isChecked property on true
setClicked(data.filter(obj => obj.isChecked))
}
My apologies in case the indentation is a bit off as I've been typing from the phone. Hope this helps!

Conditionally disable React Checkbox

I am trying to conditionally disable the checkbox in react, based on the count. Passing the value through props whether it is checked and greater than the number. I am saving the name in the state to further process it to send to in the backend database.
Here is my react code.
class CheckboxComponent extends Component {
constructor(props) {
super(props);
this.state = {
checkedItems: {}
};
}
handleChange = (event, formKey) => {
const {checkedItems} = this.state;
const checkedValues = {...checkedItems};
checkedValues[event.target.name] = event.target.checked;
this.setState((prevState, currState) => {
return {
...prevState,
checkedItems: checkedValues
}
});
};
render = () => {
const {checkedItems} = this.state;
const checkedValues = {...checkedItems};
const checkedCount = Object.values(checkedValues).length;
const checked = Object.values(checkedValues);
const disabled = checkedCount >= 3;
return (
<div>
{checkboxes.map((item, index) => (
<label className={`form__field__input__label`} key={item.key}>
<Input
type={`checkbox`}
name={item.name}
checked={this.state.checkedItems[item.name] || false}
onChange={this.handleChange}
formKey={'subjects'}
disabled={(!checked[index] && checked.length > 3)}
/>
{item.name}
</label>
))}
</div>
)
This is the Array that I am passing to render the values in the checkbox
const checkboxes = [
{
name: "Math and economics",
key: "mathsandeconomics",
label: "Math and economics"
},
{
name: "Science",
key: "Science",
label: "Science"
},
The below code snippet will work fine for you. And you can sent object to the backend having maximum of only 3 properties set to true. Get the full code from codesandbox link https://codesandbox.io/s/emmeiwhite-0i8yh
import React from "react";
const checkboxes = [
{
name: "Math and economics",
key: "mathsandeconomics",
label: "Math and economics",
},
{
name: "Science",
key: "science",
label: "Science",
},
{
name: "history",
key: "history",
label: "history",
},
{
name: "literature",
key: "literature",
label: "literature",
},
];
class CheckboxComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
checkedItems: {},
count: 0,
};
}
handleChange = (event, formKey) => {
const { name, checked } = event.target;
const updatedCheckedItems = { ...this.state.checkedItems, [name]: checked };
this.setState({
checkedItems: updatedCheckedItems,
count: Object.values(updatedCheckedItems).filter((value) => value).length,
});
};
render = () => {
const checkedValues = { ...this.state.checkedItems };
const checkedCount = Object.values(checkedValues).filter((value) => value)
.length;
console.log(this.state.checkedItems);
return (
<div>
{checkboxes.map((item, index) => (
<label className={`form__field__input__label`} key={item.key}>
<input
type={`checkbox`}
name={item.name}
checked={this.state.checkedItems[item.name] || false}
onChange={this.handleChange}
disabled={!checkedValues[item.name] && checkedCount > 2}
/>
{item.name}
</label>
))}
</div>
);
};
}
export default CheckboxComponent;
Your checked.length counts all touched boxes, not checked only. If you uncheck an input, it still will be counted. Count only true, for example Object.values(checkedValues).filter(value => value).length.
Use names instead of indexes: disabled={!checkedValues[item.name] && checkedCount > 3}
You can see full solution here: https://codesandbox.io/s/confident-http-vlm04?file=/src/App.js
event.target.getAttribute('name');
try this to get name attribute, pretty sure event.target.name is 'undefined'
I see one use case is not taken care of. checkedCount should count the number of true values only.
const checkedCount = Object.values(checkedValues).length; // existing
const checkedCount = Object.values(checkedValues).filter(item=>item==true).length //replace with this line
This would solve the problem.
Here is the code and as well as codesandbox link
Codesandbox Link
import React from "react";
export class CheckboxComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
checkedItems: {},
checkedCount: 0
};
}
handleChange = (event, formKey) => {
const { checkedItems } = this.state;
const checkedValues = { ...checkedItems };
checkedValues[event.target.name] = event.target.checked;
this.setState((prevState, currState) => {
return {
...prevState,
checkedItems: checkedValues,
checkedCount: event.target.checked
? prevState.checkedCount + 1
: prevState.checkedCount - 1
};
});
};
render = () => {
const { checkboxes } = this.props;
const { checkedCount } = this.state;
const disabled = checkedCount >= 3;
return (
<div>
<p></p>
{checkboxes.map((item, index) => (
<label className={`form__field__input__label`} key={item.key}>
<input
type={`checkbox`}
name={item.name}
checked={this.state.checkedItems[item.name] || false}
onChange={this.handleChange}
disabled={!this.state.checkedItems[item.name] ? disabled : false}
/>
{item.name}
</label>
))}
</div>
);
};
}

Cannot change redux state with a action in a component

Action on button does not perform the action to change the sorting object which sort all itens in a list (another component). I expect the button to perform this changes passing the sortBy variable on this.props.dispatch(orderBy(sortBy)) or another dynamic way without a button.
import React from 'react';
import { connect } from 'react-redux';
import orderBy from '../actions/sorting';
const TYPES = [
{ slug: "title", description: "Title" },
{ slug: "author", description: "Author" },
{ slug: "editionYear", description: "Edition Year" }
];
class BookListSorter extends React.Component {
state = {
sortBy: [{ title: "asc" }]
};
// Helper methods
getSortByKeyForIndex = index =>
Object.keys(this.state.sortBy[index] || {})[0];
getSortByValueForIndex = index =>
Object.values(this.state.sortBy[index] || {})[0];
changeSort = (key, index) => e => {
// This is what is called when an select option changes
const { target } = e; // Save target from event to use in the callback
this.setState(({ sortBy }) => {
// Use a function for atomicness - this prevents state from being out of sync
// Get the type from the event object if the onchange handler is for the type,
// otherwise get from sortBy object
const type =
key === "type" ? target.value : this.getSortByKeyForIndex(index);
// Get the direction from the event object if the onchange handler is for the direction,
// otherwise get from sortBy object
const direction =
key === "direction" ? target.value : this.getSortByValueForIndex(index);
// If both options are set, replace the indexed spot in the sortby object
// Return updated state.
return type || direction
? sortBy.splice(index, 1, { [type]: direction })
: sortBy.splice(index, 1);
});
};
filterTypes = index => ({ slug }) => {
// Filter out already used keys from previous rows
const sortByKeys = this.state.sortBy
.slice(0, index)
.reduce((keys, sortObj) => keys.concat(Object.keys(sortObj)[0]), []);
return !sortByKeys.includes(slug);
};
render() {
const { sortBy } = this.state;
const lastIndex = sortBy.length - 1;
// Only add a new row if the one above it is completely filled out
const shouldAddNewRow =
this.getSortByKeyForIndex(lastIndex) &&
this.getSortByValueForIndex(lastIndex);
const rowCount = shouldAddNewRow ? sortBy.length + 1 : sortBy.length;
return (
<div>
<h1>Choose sort order</h1>
{Array.from(Array(Math.min(rowCount, TYPES.length))).map(
(dummy, index) => (
<div>
<span>Row {index}: </span>
<select
defaultValue={this.getSortByKeyForIndex(index)}
onChange={this.changeSort("type", index)}
>
<option value="">None</option>
{TYPES.filter(this.filterTypes(index)).map(
({ slug, description }) => (
<option value={slug}>{description}</option>
)
)}
</select>
<select
defaultValue={this.getSortByValueForIndex(index)}
onChange={this.changeSort("direction", index)}
>
<option value="">None</option>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
<br />
</div>
)
)}
<br />
<button onClick={() => this.props.dispatch(orderBy(sortBy))}>sort</button>
</div>
);
}
};
const mapStateToProps = (state) => {
return {
sorting: state.sorting
};
};
//ACTIONS
//ADD BOOK
const addBook = ({ title = '', author = '', editionYear = 0} = {}) => ({
type: 'ADD_BOOK',
book: {
title,
author,
editionYear
}
});
//SORT BY
const orderBy = (order) => ({
type: 'SORT_BY',
orderBy: order
});
//book reducer
const bookReducerDefaultState = [];
const bookReducer = (state = bookReducerDefaultState, action) => {
switch(action.type) {
case 'ADD_BOOK':
return [
...state,
action.book
];
default:
return state;
};
};
//sorting reducer
const sortingReducerDefaultState = {
orderBy: [{title: 'asc'},{author: 'asc'}]
};
const sortingReducer = (state = sortingReducerDefaultState, action) => {
switch(action.type) {
case 'SORT_BY':
return {
...state,
orderBy: action.orderBy
};
default:
return state;
};
}
function compareBy(a, b, orderBy) {
const key = Object.keys(orderBy)[0],
o = orderBy[key],
valueA = a[key],
valueB = b[key];
if (!(valueA || valueB)) {
console.error("the objects from the data passed does not have the key '" + key + "' passed on sort!");
return 0;
}
if (+valueA === +valueA) {
return o.toLowerCase() === 'desc' ? valueB - valueA : valueA - valueB;
} else {
if (valueA.localeCompare(valueB) > 0) {
return o.toLowerCase() === 'desc' ? -1 : 1;
} else if (valueA.localeCompare(valueB) < 0) {
return o.toLowerCase() === 'desc' ? 1 : -1;
}
}
return 0
}
function getSortedBooks(books, orderBy) {
orderBy = Array.isArray(orderBy) ? orderBy : [orderBy];
return books.sort((a, b) => {
let result
for (let i = 0; i < orderBy.length; i++) {
result = compareBy(a, b, orderBy[i])
if (result !== 0) {
return result
}
}
return result
})
}
//store creation
const store = createStore(
combineReducers({
books: bookReducer,
sorting: sortingReducer
})
);
store.subscribe(() => {
const state = store.getState();
const sortedBooks = getSortedBooks(state.books, state.sorting.orderBy)
console.log(sortedBooks);
});
export default connect(mapStateToProps)(BookListSorter);
Can anyone help with this issue. Since the button i set up is not working?
Note: This was an answer to the original question
The best way to get the value of a select element in React is to add an onChangehandler.
In your example, it might look something like this:
<select onChange={(event) => this.setState({ firstType: event.target.value })}>
<option value="title">Title</option>
<option value="author">Author</option>
<option value="editionYear">Edition Year</option>
</select>
<select onChange={(event) => this.setState({ firstDirection: event.target.value })}>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
By changing the above select inputs, the state would look like this:
{
firstType: 'author',
firstDirection: 'desc'
}
(The state wont automatically be set until changes are made, so you would have to initialize separately.)
You would then need to transform that object into the shape you need.
This is just an example, I'll leave it up to you to transform the state into the shape that you need and to connect up redux since it looks like that's what you intend to do with the import of connect.
Note: if the option tags don't have a value attribute set, the value in event.target.value would be the content inside the tags.

How to remove a specific component in React? (Using key and id)

I was going through the React docs and was attempting to modify a TodoList Item.
https://codesandbox.io/s/43njy6458x
I am trying to remove each component with a button, however the button does not delete the item.
I have tried a few methods to filter the list, yet none have successfully accomplished the function.
const ToDo = props => (
<tr>
<td>
<label>{props.id}</label>
</td>
<td>
<input />
</td>
<td>
<label>{props.createdAt.toTimeString()}</label>
<button
type="button"
onClick={props.deleteItem}
value={props.id}
>
Delete
</button>
</td>
</tr>
);
class ToDoList extends React.Component {
constructor() {
super();
const date = new Date();
const toDoCounter = 1;
this.state = {
list: [
{
id: toDoCounter,
createdAt: date,
},
],
toDoCounter: toDoCounter,
};
}
sortByEarliest() {
const sortedList = this.state.list.sort((a, b) => {
return a.createdAt - b.createdAt;
});
this.setState({
list: [...sortedList],
});
}
sortByLatest() {
const sortedList = this.state.list.sort((a, b) => {
return b.createdAt - a.createdAt;
});
this.setState({
list: [...sortedList],
});
}
addToEnd() {
const date = new Date();
const nextId = this.state.toDoCounter + 1;
const newList = [
...this.state.list,
{id: nextId, createdAt: date},
];
this.setState({
list: newList,
toDoCounter: nextId,
});
}
addToStart() {
const date = new Date();
const nextId = this.state.toDoCounter + 1;
const newList = [
{id: nextId, createdAt: date},
...this.state.list,
];
this.setState({
list: newList,
toDoCounter: nextId,
});
}
// this is the issue
deleteItem(event) {
const clickedId = event.target.value;
//console.log(clickedId);
const arrDes = [...this.state.list];
const newList = this.state.list.filter((item) => {
//console.log(item.id);
return item.id !== clickedId;
})
this.setState({
list: newList
});
}
render() {
return (
<div>
<code>key=id index=id</code>
<br />
<button onClick={this.addToStart.bind(this)}>
Add New to Start
</button>
<button onClick={this.addToEnd.bind(this)}>
Add New to End
</button>
<button onClick={this.sortByEarliest.bind(this)}>
Sort by Earliest
</button>
<button onClick={this.sortByLatest.bind(this)}>
Sort by Latest
</button>
<table>
<tr>
<th>ID</th>
<th />
<th>created at</th>
</tr>
{this.state.list.map((todo, index) => (
<ToDo key={todo.id} value={todo.id} deleteItem={this.deleteItem.bind(this)} {...todo} />
))}
</table>
</div>
);
}
}
Isolated code in question:
// this is the issue
deleteItem(event) {
const clickedId = event.target.value;
//console.log(clickedId);
const arrDes = [...this.state.list];
const newList = this.state.list.filter((item) => {
//console.log(item.id);
return item.id !== clickedId;
})
this.setState({
list: newList
});
}
You need to cast clickedId to a natural number to match the id of the list element:
const newList = this.state.list.filter(item => {
return item.id !== +clickedId
});
The + operator is one way to do that.
Your function can be simplified:
deleteItem(event) {
// Grab the value from the clicked button by
// using destructuring to pick out that property
const { value } = event.target;
// return an array that doesn't include objects with ids
// that match the value - coerced to a number from a string
// so it matches the id type in your data
const newList = this.state.list.filter(item => item.id !== Number(value));
this.setState({ list: newList });
}

Categories