简体   繁体   中英

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.

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> 

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.

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

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 .

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM