ReactJS : Manipulating State doesn't render child components properly - reactjs

I have three Components "MyApp", "Candidate" and "Courses" and the data is passed from MyApp to Courses.
The data is as such
var data = [{
name : "azhar",
courses : ["Compilers", "Algorithms", "Data Structures"]
}, {
name : "Jenny",
courses : ["Design", "UX"]
}];
And the ReactComponents are rendered as such below :
<MyApp>
<Candidate>
<Courses>
<Courses>
<Courses>
</Candidate>
</MyApp>
Problem : There is a button to add a new Candidate, When clicked I add new Candidate to the state of MyApp. When clicked I see Child components do not render proper data.
Added JSFiddle : https://jsfiddle.net/69z2wepo/8587/
Really hoping someone gives a good explanation, because this is a simple use case.
Thanks

When you create an array of JSX elements like you do with this.state.data.map() you should always include a key attribute that is unique for that position in the array. Note that it shouldn't be something that can change like the index in the array, or the name of the candidate (since there can be duplicates).
If you add an id property on your candidate objects, and create a new id when you add a candidate, it should work: https://jsfiddle.net/qzy3sr6w/2/
Here's a good explanation: http://blog.arkency.com/2014/10/react-dot-js-and-dynamic-children-why-the-keys-are-important/
The TL;DR of that post is that the reason it works for push but not with unshift is because if you don't specify key, React will use the index of the array as key (not exactly true, but close enough). And since you're adding to the beginning of the array, it will use the index 0 which it previously used for another component, meaning that the new first element will get the second elements courses.

I don't think it is a good idea to use react.state directly, as you are doing here:
this.state.data.unshift({
name : "Alice",
courses : [] // No courses for her.
});
Better use additional variable like this:
var candidates = this.state.data;
candidates.push({
name : "Alice",
courses : [] // No courses for her.
});
this.setState({
data : candidates
});
Here is a JSFiidle: https://jsfiddle.net/69z2wepo/8597/

Related

Value of keys when no unique value is present

I have an infoBox component in React that needs to be used multiple times.
I have an array of objects.
Each value of the object needs to be passed to the infoBox component leading to the creation of an infoBox.
I am achieving this is by iterating over the object array using map(). I am able to get the output, but I was getting logs in console for unique keys.
Since my object does not have any unique value, I tried using Math.random(). Is it the right approach?
What value do I give for keys, when I do not have unique values in my objects?
userDetails = [{name : "Raj", place : "Chennai"}, {name : "Rani", place : "Mumbai"}, {name : "John", place : "Bengaluru"}]
userDetails.map((user){
return (<InfoBox details = user>);
});
Edit :
userDetails is populated dynamically say based on an organization. When the organization changes, the entire array of objects will be replaced by a new one.
Eg:
userDetails = [{name : "Tom", place : "New York"}, {name : "Pip", place : "Colombo"}, {name : "Davis", place : "Dubai"}]
When you don't have the keys of your object, You can use the index of your loop then doesn't show any warning. It's worked.
You can generate the id with indexOf
userDetails.map((user) => <InfoBox details = {user} key = {userDetails.indexOf(user)}/>)

Mutating array within an array (Polymer iron-list)

I currently have an iron-list within another iron-list. The parent's data comes from a firebase-query element, and the child's data is computed from each parent item. The db structure and code looks a bit like this:
DB: [
category1: [
itemId1: {
price: 10,
title: "title"
}
]
]
<iron-list id="categoryList" items="{{categories}}" multi-selection as="category">
<template>
<div class="category-holder">
<iron-list id="{{category.$key}}" items="{{_removeExtraIndex(category)}}" as="item" selection-enabled multi-selection selected-items="{{selectedItems}}" grid>
<template>
<div class$="{{_computeItemClass(selected)}}">
<p>[[item.title]]</p>
<p>[[item.price]]</p>
</div>
</template>
</iron-list>
</div>
</template>
</iron-list>
After selecting any number of items, the user can tap on a fab to batch edit the price. This is where I'm having issues. I can't figure out how to access the correct child iron-list in order to call list.set...I'm currently trying the following very nasty method:
var categories = this.$.categoryList;
var categoryItems = categories.items;
(this.selectedItems).forEach(function(item) {
var index = item.itemId;
categoryItems.forEach(function(itemList, categoryIndex) {
if (itemList[index]) {
categories.set('item.' + categoryIndex + '.price', 10);
}
}, this);
}, this);
I'm iterating over the selected items in order to extract the item index and then iterating over the parent iron-list data (categoryItems) in order to check if the given item exists in that subset of data. If so, then I use the category index and attempt to call set on the parent iron-list using the given path to access the actual item I want to edit. As expected, this fails. Hopefully I've made myself clear enough, any help would be appreciated!
EDIT #1:
After much experimenting, I finally figured out how to correctly mutate the child iron-list:
(this.selectedItems).forEach(function(item) {
var list = this.$.categoryList.querySelector('#' + item.category);
var index = list.items.indexOf(item);
list.set(["items", index, "price"], 30);
}, this);
A couple of things worth noting. I'm using querySelector instead of the recommended this.$$(selector) because I keep running into a "function DNE" error. But now I have another problem...after calling the function, the value gets updated correctly but I get the following error:
Uncaught TypeError: inst.dispatchEvent is not a function
Here's a picture of the full error message:
I see the light, hopefully someone can help me out!
OK, I'll take a shot at this. I think the following happens, and I guess this based on how dom-repeat works:
var categories = this.$.categoryList;
var categoryItems = categories.items;
You take the variable that the iron-list is based on, but setting one array to another just creates a reference in javascript. As soon as you update categoryItems, you also update this.$.categoryList.items. When you later sets the new value, iron-list will do a dirty check and compare all subproperties, and because they are equal (because ... reference), the iron-list wont update the dom.
What you should do is to make sure it's a totally new copy and the way of doing that is to use JSON.parse(JSON.stringify(myArray)).
Further on, one major flaw I see in your code is that you're using querySelector to select an element, and then manipulate that. What you should do is to use this.categories and only that variable.
So your method should look something like:
// Get a freshly new array to manipulate
var category = JSON.parse(JSON.stringify(this.categories);
// Loop through it
category.forEach(category) {
// update your categoryList variable
}
// Update the iron list by notifying Polymer that categories has changed.
this.set('categories', category);

How to add json array to object in existing array?

I have an object myObj.employees which looks like this:
{"employees":[{"key":1419,"rankid":8,"label":"bob (47)","exclude":false,"color":"#ffffff","textColor":"#330000","active_events":[]},{"key":1420,"rankid":8,"label":"john (48)","exclude":false,"color":"#ffffff","textColor":"#330000","active_events":[]}]}
how do I add items to active_events for key = 1419 ?
Find the 1419 entry:
const entry = myObj.employees.find(obj => obj.key === 1419);
Add entries to active events:
entry.active_events.push("more item");
If you're working in Javascript you can use the .forEach function to iterate over the employees array in a brute force manner.
myObj.employees.forEach(function(e) {
if (e.key === 1419) {
e.active_events.push("waffles")
}
});
myObj.employees["employees"][0]["active_events"].append("new event")
unless the object you showed was really just myObj in which case the answer (in python) would be:
myObj["employees"][0]["active_events"].append("new event")
let's break this down.
We take your object (myObj.employees) which gives us a dictionary with one key value pair.
We dive into ["employees"] to access the employee list.
You want to edit the [0] index employee
You want to edit the ["active_events"] value
You want to append / edit the list. I think you can take it from here.
As others have show if you do not know the index of the employee you want to edit, which you probably won't, then you'll need to loop through and look for it.
for i, employee in enumerate(myObj["employees"]):
if employee["key"] == 1419:
myObj["employees"][i]["active_events"].append('new event')

Can I check if a value is only pushed if a certain field value is not filled already?

I am trying to make a Meteor app to let users push a value to the database. It works ok, but there a small issue. As soon a certain user has pushed his information, i don't want to let the same user create another entry. Or this must be blocked, or the value the user is pushing must be overwritten for the value he is posting the second time. Now I get multiple entry's of the same user.
Here is my code. Hope you can help me here. Thanks in advance.
Estimations.update(userstory._id, {
$addToSet: {
estimations: [
{name: Meteor.user().username, estimation: this.value}
]
}
});
From the mongo docs
The $addToSet operator adds a value to an array unless the value is
already present, in which case $addToSet does nothing to that array.
Since your array elements are objects the value is the entire object, not just the username key. This means a single user can create multiple name, estimation pairs as long as the estimation value is different.
What you can do is remove any value for the user first, then reinsert:
var username = Meteor.user().username;
Estimations.update({ userstory._id },
{ $pull: { estimations: { name: username }}}); // if it doesn't exist this will no-op
Estimations.update({userstory._id },
{ $push: { estimations: { name: username, estimation: this.value }}});
By way of commentary, you've got a collection called Estimations that contains an array called estimations that contains objects with keys estimation. This might confuse future developers on the project ;) Also if your Estimations collection is 1:1 with UserStorys then perhaps the array could just be a key inside the UserStory document?

backbone routes - ordering and filtering

In my Backbone app, on my collection I have numerous sorting methods, when rendering the views based on the collection I am currently using a global var set via the route (I do it with a global as other actions add to the collection and I want the last ordering to be used). For example
routes : {
"" : "index",
'/ordering/:order' : 'ordering'
},
ordering : function(theorder) {
ordering = theorder;
listView.render();
},
then in my view
if (typeof ordering === 'undefined') {
d = this.collection.ordered();
}
else if(ordering == 'owners') {
d = this.collection.owners();
}
_.each(d, function(model){
model.set({request : self.model.toJSON()});
var view = new TB_BB.OfferItemView({model : model});
els.push(view.render().el);
});
Where ordered and owners are the 2 ordering methods.
So my first question is, based on routes could someone advice a better way of implementing above? This view gets rendered in multiple places hence me using a global rather than passing a ordered var to the method?
Second question is - I would like to also add some filtering, so lets say I want to sort by 'price' but also do some filtering (lets say by multiple categories id). How could I add a flexible 'route' to deal with filtering.
I guess I could do
routes : {
"" : "index",
'/ordering/:order/:filter1/:filter2' : 'ordering'
},
So the filter1 and filter2 would be the subsequent filtering, but if the filters could be 0 or 100 this will not work. Could anyone offer a solution?
Well, first you should be using Backbone's built-in ability to auto-sort collections. You can take advantage of this by defining a comparator function on your collection. This gives you all kinds of wins right out of the box — for example, the collection will re-sort itself every time you add or remove something from it, based on your comparator. If you want to define multiple sort functions, just define them all as functions and then update comparator when you need to. Then you can ditch that ugly global var.
For your second question, I'm not totally sure what you mean by "if the filters could be 0 or 100 this will not work." If you mean that you'll run into trouble if you don't specifiy all of the filters, then that's true. But you can use a wildcard to fix that. Here's what that might look like:
// your routes look like this:
routes : {
'/ordering/:order/filters/*filters' : 'ordering' // your routes will look like: /ordering/price/filters/filter_one/filter_two/filter_three
},
ordering: function (order, filters) {
filters = filters.split('/'); // creates an array of filters: ['filter_one', 'filter_two', 'filter_three']
listView.render(filters); // pass your filters to the view
}
// listView.render() looks like this:
render: function(filters) {
collection = this.collection;
_.each(filters, function (filter) {
collection = collection.filter(function () {
// your actual filtering code based on what the filter is
});
});
}

Resources