Reputation: 6737
I'm building a news feed using React and moment.js. Using .map
I'm rendering items with a title and content. I'd like to check if an item was posted in the same year and month as another item. If this is the case I want to hide the second items title.
Currently my code renders this:
news item one
news item two
news item three
news item four
Since item one and two share the same month and year I would like to render like this instead:
news item one
news item two
news item three
news item four
Based on this answer I've tried to find nodes with duplicate class names but I'm not quite there:
let monthNodes = [...document.querySelectorAll('.months')];
let months = []
monthNodes.map(month => {
months.push(month.className)
})
const count = names =>
names.reduce((a, b) =>
Object.assign(a, {[b]: (a[b] || 0) + 1}), {})
const duplicates = dict =>
Object.keys(dict).filter((a) => dict[a] > 1)
console.log(count(months)) // {March_2018: 2, December_2017: 1, November_2017: 1}
console.log(duplicates(count(months))) // [ 'March_2018' ]
Maybe I'm going about this the wrong way and using .map
in the first place is a bad idea?
Update
Thanks for all the great answers, it was hard to pick between them as they all work well. I have to accept David Ibl's answer since he was first to provide a working example.
Upvotes: 6
Views: 107
Reputation: 5797
Array.prototype.reduce()
can be leveraged to organise your Articles
in Object
form by year
and month
.
See below for a practical example.
// News
class News extends React.Component {
// State.
state = {
news: [
{content: 'news item one -- march 2018', date: '2018-03-02'},
{content: 'news item two -- march 2018', date: '2018-03-17'},
{content: 'news item two -- sep 2017', date: '2017-09-21'},
{content: 'news item one -- june 2017', date: '2017-06-15'}
]
}
// Render.
render = () => (
<div>
{
Object.entries(this.organised()).map(([yearmonth, articles], i) => (
<div key={i}>
<h3>{yearmonth}</h3>
{articles.map((article, j) => (
<div key={j}>
{article.content}
</div>
))}
</div>
))
}
</div>
)
// Organised.
organised = () => this.state.news.reduce((total, article) => {
const key = moment(article.date).format('YYYY MMMM')
return {
...total,
[key]: total[key] && total[key].concat(article) || [article]
}
}, {})
}
// Mount.
ReactDOM.render(<News/>,document.getElementById('container'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.21.0/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container"></div>
Upvotes: 2
Reputation: 7911
Considering that this list is coming from an API, you can have something like this once the data has arrived.
const monthwise = this.state.news.reduce((res, obj) => {
const ym = moment(obj.date).format("YYYY-MM")
res[ym] = res[ym] || []
res[ym].push(obj);
return res;
}, {})
this.setState({monthwise})
Now, in your render
, you can have:
render() {
return(
<div>
{Object.values(this.state.monthwise).map((items, index) => {
return(
<div key={index}>
<h4>{moment(items[0].date).format("YYYY MMMM")}</h4>
{items.map((item, key) => <div key={key}>{item.content}</div> )}
</div>
)
})}
</div>
)
}
Upvotes: 1
Reputation: 8509
1) You can define previousYearMonth
variable and store the previous year and month names there. You can check if your current yearMonth
and previousYearMonth
are the same and show/hide the appropriate tag:
render() {
return(
<div>
{this.state.news.map((item, index) => {
const yearMonth = moment(item.date).format("MMMM YYYY");
const yearM = moment(item.date).format("MMMM_YYYY");
const previousYearMonth = index
? moment(this.state.news[index - 1].date).format("MMMM YYYY")
: false;
return(
<div className={'months ' + yearM}>
{
(yearMonth !== previousYearMonth) && (<h2>{yearMonth}</h2>)
}
<p>{item.content}</p>
</div>
)
})
}
</div>
)
}
Check the fork of your fiddle.
2) Another way you can preprocess your data in the constructor
method and set title
property this way:
constructor(props) {
super(props);
const data = [
{content: 'news item one', date: '2018-03-02'},
{content: 'news item two', date: '2018-03-17'},
{content: 'news item two', date: '2017-09-21'},
{content: 'news item one', date: '2017-06-15'}
];
data.forEach((item, index) => {
const yearMonth = moment(item.date).format("MMMM YYYY");
const previousYearMonth = index
? moment(data[index - 1].date).format("MMMM YYYY")
: false;
if (yearMonth !== previousYearMonth) {
item.title = yearMonth
}
});
this.state = {
news: data
}
}
In this case, in the render
method you can just check that title
property exists and show title:
<div>
{
item.title && (<h2>{item.title}</h2>)
}
<p>{item.content}</p>
</div>
Check the fiddle with this approach.
Upvotes: 1
Reputation: 17220
Assuming the data structure looked like this:
const data = [
{
date: 'March 2018',
item: {
title: 'news item one'
}
},
{
date: 'March 2018',
item: {
title: 'news item two'
}
},
{
date: 'September 2017',
item: {
title: 'news item two'
}
},
{
date: 'June 2017',
item: {
title: 'news item one'
}
}
]
You could use Array.reduce:
const formatted = data.reduce((obj, item) => {
obj[item.date] = obj[item.date] ? [...obj[item.date], item] : [item]
return obj
}, {})
Upvotes: 1
Reputation: 911
you can do it this way: https://jsfiddle.net/2e27jvvh/
render() {
const arr = new Array();
return(
<div>
{this.state.news.map(item => {
const yearMonth = moment(item.date).format("MMMM YYYY");
const yearM = moment(item.date).format("MMMM_YYYY");
if (arr.indexOf(yearMonth) < 0){
arr.push(yearMonth);
return (
<div className={'months ' + yearM}>
<h2>{yearMonth}</h2>
<p>{item.content}</p>
</div>
)
} else {
return (
<div className={'months ' + yearM}>
<p>{item.content}</p>
</div>
)
}
})
}
</div>
)
}
Maybe you should sort items based on year and month before to ensure correct behaviour even when items are not sorted initially. But this works with your example.
Upvotes: 1