Pixelomo
Pixelomo

Reputation: 6737

Checking if items within mapped array share values

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.

Please see my fiddle

Currently my code renders this:

March 2018

news item one

March 2018

news item two

September 2017

news item three

June 2017

news item four

Since item one and two share the same month and year I would like to render like this instead:

March 2018

news item one

news item two

September 2017

news item three

June 2017

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

Answers (5)

Arman Charan
Arman Charan

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.

JSFiddle

// 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

tanmay
tanmay

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>
  )
}

Working jsfiddle

Upvotes: 1

Mikhail Shabrikov
Mikhail Shabrikov

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

Mike
Mike

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
}, {})

Example Codepen

Upvotes: 1

David Ibl
David Ibl

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

Related Questions