Delete objects from array in state using unique ID - javascript

I have list which elements must be deletable (for example with delete button). How can I realize that from react?
this is my state:
state = {
infos: [
{
id: 1,
info: 'some info',
deleted: false
},
{
id: 2,
info: 'some info',
deleted: false
},
{
id: 3,
info: 'some info',
deleted: false
}
]
}
this is a function for delete that I tried:
removeInfo() {
this.state.infos.splice(key, 0)
}
this is a jsx code that I get after maping:
{
this.state.infos.map((item, key) => {
return (
<ListItem key={item.key + key}>
<Icon color="gray" f7="home" />
<span className="text-black">{item.info}</span>
<Button><Icon f7="edit" color="#39b549" /></Button>
<Button onClick={this.removeInfo}><Icon color="black" f7="trash" /></Button>
</ListItem>
)
})
}

You need few changes.
First we need to pass the id of item we want to remove to the remove function:
<Button onClick={()=>this.removeInfo(item.id)}><Icon color="black" f7="trash" /></Button>
Then you need to remove the item from array in immutable way using setState.
removeInfo(id) {
this.setState(ps=>({infos:ps.infos.filter(x=>x.id!=id)}))
}
splice mutates the array.

You need to use setState and note that you canĀ“t mutate the state so you need to use the spread operator to create a new array.
function removeInfo(index) {
this.setState((prev) => ({
infos: [...prev.infos.slice(0, index), ...prev.infos.slice(index+1)]
}))
}

Related

I'm trying to add to an array of objects that is broken into two inputs in React

So I have an array of objects where the keys are 'cost' and 'service' called estimate. You can add to the array by clicking 'Add' which adds a new index (i) to the array. The issue is on the first cycle I get a good array of {'cost': 2500, 'service': "commercial cleaning"} (imgSet-1) but when I add another item it completely erases the array and sets only one of the nested objects key and value. (imgSet-2). This is the outcome I'm looking for once the state has been saved (imgSet-3) I have tried going with #RubenSmn approach but then I receive this error. (imgSet-4)
imgSet-1 *********
Adding an initial service
Outcome of the initial service addition
imgSet-2 *********
Adding the second service
Outcome of the second service addition
imgSet-3 *********
imgSet-4 *********
Below is the code for the part of the page where you can add services and the output of the text inputs.
const [estimate, setEstimate] = useState([]);
{[...Array(numServices)].map((e, i) => {
return (
<div key={i} className="flex justify-between">
<div>
<NumericTextBoxComponent
format="c2"
name={`cost-${i}`}
value={estimate?.items?.["cost"]?.[i]}
change={(e) =>
setEstimate({ ...estimate, items: [{...estimate?.items?.[i],cost: e?.value}]})
}
placeholder='Price'
floatLabelType="Auto"
data-msg-containerid="errorForCost"
/>
</div>
<div>
<DropDownListComponent
showClearButton
fields={{ value: "id", text: "service" }}
name={`service-${i}`}
value={estimate?.items?.["service"]?.[i]}
change={(e) =>
setEstimate({ ...estimate, items: [{...estimate?.items?.[i],service: e?.value}]})
}
id={`service-${i}`}
floatLabelType="Auto"
data-name={`service-${i}`}
dataSource={estimateData?.services}
placeholder="Service"
data-msg-containerid="errorForLead"
></DropDownListComponent>
<div id="errorForLead" />
</div>
</div>
);
})}
</form>
<button onClick={() => setNumServices(numServices + 1)}>Add</button>
I have tried multiple variations of spread operators but I can't seem to get it to work. My expected result would be:
estimate:{
items: [
{'cost': 2500, 'service': 'Commercial Clean'},
{'cost': 500, 'service': 'Bathroom Clean'},
{'cost': 180, 'service': 'Apartment Clean'},
{etc.}
]
}
The initial state is an array which is not the object you're setting in the change handlers. You can have an initial state like this.
const [estimate, setEstimate] = useState({ items: [] });
You're not adding back the old items of the state when you're setting the new state.
setEstimate({
...estimate,
items: [{ ...estimate?.items?.[i], cost: e?.value }],
// should be something like
// items: [...estimate.items, { ...estimate.items?.[i], cost: e?.value }],
});
But you can't do that since it will create a new object in your items array every time you change a value.
I made this dynamic handleChange function which you can use for you state changes. The first if statement is to check if the itemIndex is already in the items array. If not, create a new item with the propertyName and the value
const handleChange = (e, itemIndex, propertyName) => {
const newValue = e?.value;
setEstimate((prevEstimate) => {
if (prevEstimate.items.length <= itemIndex) {
const newItem = { [propertyName]: newValue };
return {
...prevEstimate,
items: [...prevEstimate.items, newItem]
};
}
// loop over old items
const newItems = [...prevEstimate.items].map((item, idx) => {
// if index' are not the same just return the old item
if (idx !== itemIndex) return item;
// else return the item with the new service
return { ...item, [propertyName]: newValue };
});
return {
...prevEstimate,
items: newItems,
};
});
};
For the Service dropdown, you can do the same for the Cost just change the property name
<DropDownListComponent
...
value={estimate.items[i]?.service}
change={(e) => handleChange(e, i, "service")}
...
></DropDownListComponent>
See here a simplified live version

How can I add new elements to a list?

I was trying to add a new element of array to the list with update of one property (id). I want to make it 1 more than length of array.
But I get some weird outputs, with add every new object. All elements are getting array.length +1 value.
I made several variations of this code with let, const or even operating directly on this.state.produktsToBuy, and every time I got the same output
handleAddToShop = (produktToBuy) => {
const id = this.state.produktsToBuy.length+1;
produktToBuy.id = id + 1;
const produktsToBuy = this.state.produktsToBuy;
produktsToBuy.push(produktToBuy);
this.setState({produktsToBuy});
};
I Should get the output as 1,2,3,4,5,6,7
But on the end I get 7,7,7,7,7,7
Make sure you're not mutating the state directly. In JS, objects are a reference type. When you assign this.state.produktsToBuy to const produktsToBuy and push something to produktsToBuy, you're actually pushing to the original this.state.produktsToBuy and you modify the state.
You can use the spread operator (...) to create a shallow copy of the state (produktsToBuy):
class App extends React.Component {
state = {
items: [
{ name: "test item 1", price: 4.99 },
{ name: "test item 2", price: 7.99 },
{ name: "test item 3", price: 19.99 }
],
produktsToBuy: []
};
handleAddToShop = (produktToBuy) => {
this.setState((prev) => ({
produktsToBuy: [
...prev.produktsToBuy,
{
...produktToBuy,
id: prev.produktsToBuy.length + 1
}
]
}));
};
render() {
return (
<div className="App">
<div style={{ display: "flex" }}>
{this.state.items.map((item) => (
<div
key={item.name}
style={{
border: "1px solid #ccc",
margin: "1rem",
padding: "1rem",
textAlign: "center"
}}
>
<h3>{item.name}</h3>
<p>${item.price}</p>
<button onClick={() => this.handleAddToShop(item)}>Add</button>
</div>
))}
</div>
<pre>{JSON.stringify(this.state.produktsToBuy, null, 2)}</pre>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You should be maintaining all of the previous state if there's anything other than just the produktsToBuy. Also, you always need the functional form of setState if anything you're setting is dependent on the previous state(as is OFTEN the case). And, like Zsolt said, you never mutate the state directly in React. Here's my answer (very similar to #Zsolt Meszaros'). Note: .concat creates a new array, so we don't have to worry about mutating the original.
handleAddToShop = (produktToBuy) => {
this.setState((prevState) => {
const { produktsToBuy } = prevState;
return {
...prevState,
produktsToBuy: produktsToBuy.concat([
{
...produktToBuy,
id: produktsToBuy.length + 1,
},
]),
};
});
};

How to render react component from a json object?

I made a menu component that accepts a json object with all menu itens.
For icons i use react-icons/io.
The json object is something like this:
const menus = {
Item1: { buttonText: 'Item 1 text', icon: { IoLogoAndroid }, path: 'item 1 path' },
Item2: { buttonText: 'Item 2 text', icon: { IoLogoAndroid }, path: 'item 2 path'},
};
This is the Menu function that will render the menu items as buttons:
const buttons = Object.keys(this.props.menus).map((menu) => {
return (
<a key={menu} href={this.props.menus[menu].path}
onClick={this.changeMenu.bind(this, menu)}>
{...this.props.menus[menu].icon} <- fail here
{this.props.menus[menu].buttonText}
</a>
);
})
I tried many ways to render the icon, but i am clueless on how this could work. Not sure if this is even possible. Any pointers?
If you are importing the icon from where you are defining the object then just tag it <IoLogoAndroid/>;, so react knows it should treat it as an element to render.
const menus = {
Item1: { buttonText: 'Item 1 text', icon: <IoLogoAndroid/> , path: 'item 1 path' },
Item2: { buttonText: 'Item 2 text', icon: <IoLogoAndroid/>, path: 'item 2 path'},
};
And then just call it directly (remove the ...)
<a key={menu} href={this.props.menus[menu].path}
onClick={this.changeMenu.bind(this, menu)}>
{this.props.menus[menu].icon}
{this.props.menus[menu].buttonText}
</a>
Alternatively you could just call React.createElement if you don't want to tag it in your object definition.
<a key={menu} href={this.props.menus[menu].path}
onClick={this.changeMenu.bind(this, menu)}>
{React.createElement(this.props.menus[menu].icon)}
{this.props.menus[menu].buttonText}
</a>
Here is a sample showing the 2 implementations https://codesandbox.io/s/pmvyyo33o0
I was just working with a similar project, and I managed to make it work, with a syntax like this
I here have an array of objects (like yours)
links: [
{
name: 'Frontend',
link: 'https://github.com/Thomas-Rosenkrans-Vestergaard/ca-3-web',
icon: <FaCode size={40} />,
id: 1
},
{
name: 'Backend',
link: 'https://github.com/Thomas-Rosenkrans-Vestergaard/ca-3-backend',
icon: <FaCogs size={40}/>,
id: 2
},
{
name: 'Mobile',
link: 'https://github.com/Thomas-Rosenkrans-Vestergaard/ca-3-app',
icon: <FaMobile size={40} />,
id: 3
}]
I then render my component by mapping, where I pass in the entire object as a prop
const projects = this.state.projects.map((project, i) => {
return(
<Project key={`Project key: ${i}`} project={project} />
)
})
I then use objectdestructing to get the prop
const { logo } = this.props.project
then it can just be displayed
//in my case I use antd framework, so I pass the FAIcon component in as a prop
<Meta
avatar={logo}
title={title}
description={description}
/>
I suppose you could do the same thing, by just passing the entire menu object in as a prop, and then accessing the icon?
You need to change the following:
icon: <IoLogoAndroid />
And in the code (remove the spread operator):
this.props.menus[menu].icon
Also, a few refactoring suggestions.
Do you really need to have an object of objects? Why not an array of objects, where every item has a "name" prop? It will be easier to iterate through, as you can access props directly from map, unlike with object keys.
You are creating a button list, so you should have ul and li tags aswell.
Consider passing only a reference to onClick such as:
onClick={this.changeMenu}
If you need to pass data around, you should use dataset for that. Pass a name/path then find it inside the change handler to avoid rebinding inside every re-render.
Refactored suggestion with an array of objects
changeMenu = e => {
const { menus } = this.props;
const { menuName } = e.target.dataset;
const menu = menus.find(menu => menu.name === menuName);
// Do something with menu
return;
};
renderMenu() {
return (
<ul>
{this.props.menus.map(menu => (
<li key={menu.name} style={{ listStyle: "none" }}>
<a
data-menu-name={menu.name}
href={menu.path}
onClick={this.changeMenu}
>
{menu.icon}
{menu.buttonText}
</a>
</li>
))}
</ul>
);
}

How to bind an object to an input tag than just a value with ReactJS?

I'm new to ReactJS. I have an input tag that suggests possible values as the user types in the input. This suggestion list comes from an API in which each item in the list has attributes like below:
item: {
'id': 'some_id',
'name': 'some_name',
'att1': 'some_att_1',
'att2': 'some_att_2'
}
In my code, I render the 'name' attribute as the value in the input and 'id' as the key as shown below:
renderItem={(item, isHighlighted) => (
<div
key={item.id}
style={{ background: isHighlighted ? "lightgray" : "white" }}>
{item.name}
</div>
)}
So far so good. Then I have event handlers as shown below:
onChange={e => {
this.setState({ from: e.target.value });
// do something here...
}}
onSelect={val => this.setState({ from: val })}
renderMenu={function(items, value, style) {
return (
<div
style={{ ...style, ...this.menuStyle, zIndex: 10 }}
children={items}
/>
);
}}
As you can see, in the e.target I can access the original item's id or name attributes. However, I need to access the att1 and att2 because I need to use them as parameters when the user selects the input and clicks on a button. How can I do this? Thanks.
You can use find
onChange={e => {
this.setState({ from: e.target.value });
let item = obj.find(val => val.id === e.target.value); // supposing item are your array of objects
// and e.target.value is the item id
}}
var obj = [{
'id': 'some_id',
'name': 'some_name',
'att1': 'some_att_1',
'att2': 'some_att_2'
},{
'id': 'some_id1',
'name': 'some_name1',
'att1': 'some_att_11',
'att2': 'some_att_22'
}
];
let item = obj.find(val => val.id === 'some_id1');
console.log("att1", item.att1);
console.log("att2", item.att2);
I'm not completely sure how your components are setup so I am going to take an approach that will work regardless.
If you have access to the ID of the selected value and your data is in an array, in your onChange event you can write
onChange={e => {
this.setState({ from: e.target.value });
// This is where I'm unsure, is your ID e.target.value?
const id = 0
// I'm only using the first item in the filtered array
// because I am assuming your data id's are unique
const item = items.filter(item=>item.id===id)[0]
// Now down here you can access all of the item's data
}}
Using a library like lodash you could find the value using the name or the id. Here is an example:
const items = [{
'id': 'some_id',
'name': 'some_name',
'att1': 'some_att_1',
'att2': 'some_att_2'
}]
console.log(_.find(items, { name: 'some_name'}))
/* Returns
{
"id": "some_id",
"name": "some_name",
"att1": "some_att_1",
"att2": "some_att_2"
}
*/
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.4/lodash.min.js"></script>

How I can prepend an element after each child in a react jsx render function?

I have the following code:
class MessageList extends Component {
render () {
return (
<ul>
{
this.props.messages.map((message) => {
return <Message key={message.id} message={message} />
})
}
{
this.props.replied_messages.map((message) => {
return <Message key={message.id} message={message} />
})
}
</ul>
)
}
}
I want to add this.props.replied_messages.map after each child on this.props.messages.map.
Example data:
let initialState = {
messages: [
{
id: 0,
timestamp: 1464092864076,
text: 'holas'
},
{
id: 1,
timestamp: 1464125678372,
text: 'other comment'
}
],
replied_messages: [
{
id: 0,
message_replied_id: 0,
timestamp: 1464092864076,
text: 'eyyy'
},
{
id: 1,
message_replied_id: 0,
timestamp: 1464125851108,
text: 'a reply'
},
{
id: 2,
message_replied_id: 1,
timestamp: 1464125909151,
text: 'other comment reply'
}
]
}
I want to add this.props.replied_messages.map after each child on
this.props.messages.map.
What it sounds like you want to do is render a threaded message list. If you want to render a flat list of <Message> components where all replies follow their parent message, you could do this with a nested map: map over the messages, then get all replies to that message using a filter on the replied_messages, then combine both (message + replies for that message) into an array and map the array to <Message> components.
<ul>
{ this.props.messages.map(message =>
[
message,
...this.props.replied_messages.filter(reply => reply.message_replied_id == message.id)
].map(message => <Message key={message.id + "_" + message.message_replied_id} message={message}/>)
) }
</ul>
Example in CodePen.
(Note that since your messages and replied_messages have duplicate id values, I had to make the key based on both. Preferably you would not have duplicate ids like this.)
But if I were you I would just build a threaded message list first, and render a hierarchical component tree with replies as children of their parent message. For example, you can build the threaded messages from your two arrays like this:
function messagesToThreads(messages, replied_messages) {
return messages.map(message => {
return {
message,
replies: replied_messages.filter(reply => reply.message_replied_id == message.id)
}
});
}
And render nested <Message> items to create a hierarchical list:
<ul>
{ messagesToThreads(this.props.messages, this.props.replied_messages).map(thread =>
<Message key={thread.message.id} message={thread.message}>
{ thread.replies.map(reply => <Message key={reply.id} message={reply} />) }
</Message>
) }
</ul>
Example in CodePen.

Categories

Resources