How to order ascending by highest price using React? - reactjs

I have a component that receives offers and want to set the highest on top. This is the code:
var Offerslist = React.createClass({
_renderOffers: function(key) {
var details = this.props[key];
// TODO: set order of highest offer descending
return (
<li key={key} className="offer-of-item">
€ {details.price}
<a className="remove-offer" onClick={this._removeOffer}>X</a>
</li>
)
},
render : function() {
return (
<ol className="list-of-offers-per-item">
{Object.keys(this.props).map(this._renderOffers)}
</ol>
)
}
});
I thought of populating an array with objects {price:123, offerid:'foo123'} and then use .sort by price. But I can't populate an array like this as it never iterates through all objects at once. Is there perhaps some built-in React way to do this?
data:

For a ordinary array you'd use the sort method. But since your offers is an object, not an array, you can sort the keys by looking up the price in the offers object like this:
Object.keys(this.props)
.sort((a, b) => ( this.props[a].price - this.props[b].price ))
.map( this._renderOffers)

Related

React – how to conditionally return one value (or a fallback)

I have this code (edited for brevity):
export default function Navigation({ page }) {
return (
<ul>
{page.category.map((category) => (
<>
{(category.slug === exampleSlug)
? <li key={category._id}>{category.title}</li>
: <li key={category._id}>All</li>
}
</>
))}
</ul>
)
}
It correctly returns either a category title or "All" for each category that a page has. Instead, I need it to return only one value (as opposed to one for each category). In plain text, if any one category slug matches the exampleSlug, display category title, otherwise display "All".
I've tried to solve it by using a for loop instead of a map method, such as if a condition is met return a value, otherwise fall back to another value, but it turns out that in this case I wouldn't be able to use return, only Array.push (please correct me if I'm wrong).
Thank you
If you only ever want to render exactly one <li>, either for the first category that matches or some fallback, then simply look for a matching category, and decide on whether one exists:
export default function Navigation({page}) {
// find a matching category, it will be undefined if none exits
const category = page.category.find(category => category.slug === exampleSlug);
return (
<ul>
{/* render, based on whether a category was found */}
<li>{undefined === category ? 'All' : category.title}</li>
</ul>
);
}
You could createa function anyoneMatches that takes page.category, exampleSlug and uses find to find the first categry that matches exampleSlug.
But you have a problem: in case matches, what category.title you want to show? The first that matches? If yes, you have to create a state variable called category and set it when you find the match. Something like:
const [category, setCategory] = useState({"_id": 0}); //<-- first match category
export default function Navigation({ page }) {
return (
<ul>
{anyoneMatches(page.category, exampleSlug)
? <li key={category._id}>{category.title}</li>
: <li key={category._id}>All</li>
}
</ul>
)
}
const anyoneMatches = (categories, exampleSlug) => {
let cat = categories.find(cat => cat.slug === exampleSlug); //<-- find first category that matches
if (cat) setCategory(cat); //<-- if exists, call setCategory
else setCategory({"_id": 0}); // else set category to default category (we need _id for li's key)
return cat !== undefined; //<-- return a bool to condition html
}

Mapping Double nested JSON - Object not allowed in React Child

I have a json file that looks like this:
"skills":[
{
"type":"Languages",
"skill":[
{
"name":"Python"
},
{
"name":"Java"
},
{
"name":"JavaScript"
},
{
"name":"HTML"
},
{
"name":"Bash"
},
{
"name":"MySQL"
}
]
},
{
"type": "Flavours",
"skill": [
{
"name":"Reactjs"
},
{
"name":"Angularjs"
},
{
"name":"Agile"
},
{
"name":"Waterfall"
},
{
"name":"Kanban"
}
]
},
{
"type": "Technologies",
"skill": [
{
"name":"Jira"
},
{
"name":" BitBucket"
},
{
"name":"Git"
}
]
}
]
},
And i am trying to render it using a nested mapping function:
var skills = this.props.data.skills.map((skills) =>{
var skill = skills.skill.map(function(skill){
return <li key={skill.name}>{skill}</li>
})
return <ul key={skills.type}>{skills}</ul>
})
However it says "Error: Objects are not valid as a React child (found: object with keys {name}). If you meant to render a collection of children, use an array instead."
So i tried it like this:
var skills = this.props.data.skills.map(function(skills){
var skillsArr = []
var skill = skills.skill.map(function(skill){
var skillArr = []
skillArr.push(<li key={skill.name}>{skill}</li>)
return <span>{skillArr}</span>
})
skillsArr.push(<div key={skills.type}><h3>{skills.type}</h3>
<ul>
{skill}
</ul>
</div>);
return <div>{skillsArr}</div>
})
But this too gives me the exact same error, i dont get what is wrong here because if i do a single mapping of just the skill types it works, it is only when i try to render the inner mapped items does this error occur and break my code
This is how i am calling it btw:
<div className="bars">
<ul className="skills">
{skills}
</ul>
</div>
If we are talking about using React, you should think more about how to organize your code in order to follow a proper component structure, that will let clear what you want to render and how to properly split your data and responsibilities.
Looking to your JSON, we have a set of "skills" that have skills inside it (let's call them "innerSkills").
We can easily split it into 3 components, let's think together:
We can have a List that will render all your Skills.
We can have a Skill that will be responsible for rendering each Skill data, inside it, we will need to render the InnerSkills, so let's split it to another component.
We have then InnerSkill, that will be responsible for rendering each innerSkill that we have for each skill.
So, long story short, what we have is:
List -> Skill -> InnerSkills
Great, now that we established the split, let's see how we can make each component responsible for rendering its data.
Let's say we want to simply call <List skills={data} />. Following this, we can then start on the list itself, which would look something like:
const List = ({ skills }) => (
<ul>
{skills.map((skill, i) => (
<Skill key={i} skill={skill} />
))}
</ul>
);
Now that we are looping through all Skills and calling the Skill component for rendering it, we can take a look at how Skill should look, since it will also need to loop through skill.
const Skill = ({ skill }) => (
<li>
<p>Type: {skill.type}</p>
<ul>
{skill.skill.map((innerSkill, i) => (
<InnerSkill key={i} innerSkill={innerSkill} />
))}
</ul>
</li>
);
Great. Now we already have the two loops you need to render all the data, so it's just missing the definition on how each InnerSkill should look like, and we can take a simplified approach and say we just want to render the name, so it could be something like:
const InnerSkill = ({ innerSkill }) => (
<li>
<p>Name: {innerSkill.name}</p>
</li>
);
To summarize this implementation, I made a simple code sandbox so you can See it live! and play around with the components.
I hope this clarifies your question and helps you to think better in the future on how you want to organize stuff, first check how to split, later how to render. Don't try to start rendering everything inside loops because it will get nasty.
There are two things in your code causing this error:
var skills = this.props.data.skills.map((skills) =>{
var skill = skills.skill.map(function(skill){
// should be <li key={skill.name}>{skill.name}</li>
return <li key={skill.name}>{skill}</li>
})
// should be <ul key={skills.type}>{skill}</ul>
return <ul key={skills.type}>{skills}</ul>
})
Assuming you want a single unordered list of all skill names, I'd suggest using the flatMap() function to re-write this as follows:
<div className="bars">
<ul className="skills">
{this.props.data.skills.flatMap((skillGroup) =>
skillGroup.skill.map((skill) => (
<li key={skill.name}>{skill.name}</li>
))
)}
</ul>
</div>

React/JSX - How to render a list alphabetically

I am working on someone else's code and trying to figure out how to render a list alphabetically in React. I didn't write any of this and have very little knowledge of React, so please bear with me.
The ul looks like this:
<ul className="prod-group__filter__options" ref={(options) => this.options = options}>
{
filter.dropdown.map((option, index) => (
this.renderOption(filter, option, index)
))
}
</ul>
and the renderOption function, which obviously renders the list items looks like this:
renderOption(filter, option, index) {
return (
<li className="prod-group__filter__option" key={index}>
<label className="prod-group__filter__option__label">
<input name={filter.name}
type="checkbox"
defaultValue={option.slug}
checked={option.checked}
onChange={this.optionChangeHandler} />
{option.label}
</label>
</li>
);
}
The value I am trying to alphabetize is option.slug which is coming from a json list. Can anyone help me get a bit closer to rendering this list alphabetically?
It looks like filter.dropdown is your array of options. This array is passed to the .map() method which then runs a renderOption method in the given order.
Hence, you should sort the filter.dropdown array in your ul component code just before calling .map() on it.
You will have to sort option alphabetically using plain javascript before calling filter.dropdown.map... on it. I would advice using lodash function _.sortBy(option, 'slug'); where option is an array of objects with a property called slug then you can pass the sorted result to your map function.
In case anyone is interested, the solution was to sort the list items before calling .map() on it as both macbem and finch suggested. I used const to create an "items" constructor that I could then pass into the ul further down:
const items = [].concat(this.props.options)
.sort((a, b) => {
const One = a.slug.toUpperCase();
const Two = b.slug.toUpperCase();
return (One < Two) ? -1 : (One > Two) ? 1 : 0;
})
.map((option, index) => this.renderOption(name, option, index));
return (
<div className={classes}>
<a className="prod-group__filter__dropdown"
ref={(trigger) => this.trigger = trigger}
onClick={this.triggerClickHandler}>
{label}
</a>
<ul className="prod-group__filter__options" ref={options => this.options = options}>
{ items }
</ul>
</div>
);

reactjs: no key assigned warning, but only for the first iteration

I have 3 components:
Main
-- adItem
-- Offerslist
--- offers (just li's, not a comp)
//On the main comp containing state I add the offer and I pass the function down as a prop:
_addOffer: function(offerData) {
var timestamp = (new Date()).getTime();
// add offer to item
this.state.advitems[offerData.itemId].offers['offer-id' + timestamp] = offerData;
this.setState({
advitems: this.state.advitems
});
},
/*
AdItem
*/
var AdItem = React.createClass({
_addOffer: function(event) {
var timestamp = (new Date()).getTime();
var offerData = {
offerId: 'offer-' + timestamp,
itemId: this.props.index,
price: this.refs.offerprice.value
};
},
render : function() {
//some code to hide and show with css classes
{renderOffers}
)
}
});
var Offerslist = React.createClass({
_renderOffers: function(key) {
var details = this.props[key];
return (
<li className="offer-of-item">
€ {details.price}
<a className="remove-offer" onClick={this._removeOffer}>X</a>
</li>
)
},
render : function() {
return (
<ol className="list-of-offers-per-item">
{Object.keys(this.props).map(this._renderOffers)}
</ol>
)
}
});
AdItem can contain multiple offers. When I enter the first offer, I get the warning that it should contain a unique key. I checked and it DOES have a unique key. When adding the second, I don't get the warning. But I'm unsure whether it just stops warning me or it's corrected in some way.
Am I doing something wrong here? I'm unsure of what to check next.
When you iterate an array and create elements dynamically, you also have to add a key prop to said elements:
{Object.keys(this.props).map(this._renderOffers)}
this._renderOffers must return elements with keys that are unique relative to each other. The shortcut is to simply use the index in the array, but a much better approach that ensures that React knows how to properly tell one element from another is to use the offer item's id, if you know that it's unique:
_renderOffers: function(key) {
var details = this.props[key];
return (
/* Add a key prop here. E.g. details.itemId */
<li key={details.itemId} className="offer-of-item">
€ {details.price}
<a className="remove-offer" onClick={this._removeOffer}>X</a>
</li>
)
},
This makes sure that when you add or remove items, or alter the data, React only manipulates the corresponding DOM element, instead of redrawing the entire list, or potentially alter the wrong element.

Custom sort function in ng-repeat

I have a set of tiles that display a certain number depending on which option is selected by the user. I would now like to implement a sort by whatever number is shown.
The code below shows how I've implemented it (by gettting/setting a value in the parent cards scope). Now, because the orderBy function takes a string, I tried to set a variable in the card scope called curOptionValue and sort by that, but it doesn't seem to work.
So the question becomes, how to I create a custom sort function?
<div ng-controller="aggViewport" >
<div class="btn-group" >
<button ng-click="setOption(opt.name)" ng-repeat="opt in optList" class="btn active">{{opt.name}}</button>
</div>
<div id="container" iso-grid width="500px" height="500px">
<div ng-repeat="card in cards" class="item {{card.class}}" ng-controller="aggCardController">
<table width="100%">
<tr>
<td align="center">
<h4>{{card.name}}</h4>
</td>
</tr>
<tr>
<td align="center"><h2>{{getOption()}}</h2></td>
</tr>
</table>
</div>
</div>
and controller :
module.controller('aggViewport',['$scope','$location',function($scope,$location) {
$scope.cards = [
{name: card1, values: {opt1: 9, opt2: 10}},
{name: card1, values: {opt1: 9, opt2: 10}}
];
$scope.option = "opt1";
$scope.setOption = function(val){
$scope.option = val;
}
}]);
module.controller('aggCardController',['$scope',function($scope){
$scope.getOption = function(){
return $scope.card.values[$scope.option];
}
}]);
Actually the orderBy filter can take as a parameter not only a string but also a function. From the orderBy documentation: https://docs.angularjs.org/api/ng/filter/orderBy):
function: Getter function. The result of this function will be sorted
using the <, =, > operator.
So, you could write your own function. For example, if you would like to compare cards based on a sum of opt1 and opt2 (I'm making this up, the point is that you can have any arbitrary function) you would write in your controller:
$scope.myValueFunction = function(card) {
return card.values.opt1 + card.values.opt2;
};
and then, in your template:
ng-repeat="card in cards | orderBy:myValueFunction"
Here is the working jsFiddle
The other thing worth noting is that orderBy is just one example of AngularJS filters so if you need a very specific ordering behaviour you could write your own filter (although orderBy should be enough for most uses cases).
The accepted solution only works on arrays, but not objects or associative arrays. Unfortunately, since Angular depends on the JavaScript implementation of array enumeration, the order of object properties cannot be consistently controlled. Some browsers may iterate through object properties lexicographically, but this cannot be guaranteed.
e.g. Given the following assignment:
$scope.cards = {
"card2": {
values: {
opt1: 9,
opt2: 12
}
},
"card1": {
values: {
opt1: 9,
opt2: 11
}
}
};
and the directive <ul ng-repeat="(key, card) in cards | orderBy:myValueFunction">, ng-repeat may iterate over "card1" prior to "card2", regardless of sort order.
To workaround this, we can create a custom filter to convert the object to an array, and then apply a custom sort function before returning the collection.
myApp.filter('orderByValue', function () {
// custom value function for sorting
function myValueFunction(card) {
return card.values.opt1 + card.values.opt2;
}
return function (obj) {
var array = [];
Object.keys(obj).forEach(function (key) {
// inject key into each object so we can refer to it from the template
obj[key].name = key;
array.push(obj[key]);
});
// apply a custom sorting function
array.sort(function (a, b) {
return myValueFunction(b) - myValueFunction(a);
});
return array;
};
});
We cannot iterate over (key, value) pairings in conjunction with custom filters (since the keys for arrays are numerical indexes), so the template should be updated to reference the injected key names.
<ul ng-repeat="card in cards | orderByValue">
<li>{{card.name}} {{value(card)}}</li>
</ul>
Here is a working fiddle utilizing a custom filter on an associative array: http://jsfiddle.net/av1mLpqx/1/
Reference: https://github.com/angular/angular.js/issues/1286#issuecomment-22193332
The following link explains filters in Angular extremely well. It shows how it is possible to define custom sort logic within an ng-repeat.
http://toddmotto.com/everything-about-custom-filters-in-angular-js
For sorting object with properties, this is the code I have used:
(Note that this sort is the standard JavaScript sort method and not specific to angular) Column Name is the name of the property on which sorting is to be performed.
self.myArray.sort(function(itemA, itemB) {
if (self.sortOrder === "ASC") {
return itemA[columnName] > itemB[columnName];
} else {
return itemA[columnName] < itemB[columnName];
}
});
To include the direction along with the orderBy function:
ng-repeat="card in cards | orderBy:myOrderbyFunction():defaultSortDirection"
where
defaultSortDirection = 0; // 0 = Ascending, 1 = Descending

Resources