Donny groezinger
Donny groezinger

Reputation: 147

Angular/RXJS, How do I sort an Observable Array of objects based on a SPECIFIC String value?

I have an interesting problem (I think) and would love some help.

I am returning an Observable[DisplayCard[]] that is filtered based on a value. That all works great. After I filter that array based on a value, I NOW want to sort the returned array by another one of it's values called category. category can only be one of three String values: ONGOING, UPCOMING, and PAST.

I want to return an array that will sort like this: ONGOING --> UPCOMING --> PAST. So, all the display cards that have "ONGOING" will be in front, after there is no more of those, I want all the "UPCOMING" cards and then after those, I want all the "PAST" cards to show.

Here is my code:

displayCommunityCards$ = this.cardService.displayCards$
    .pipe(
      map(communityEventsCards => communityEventsCards.filter(community => community.type === 'COMMUNITY EVENT')
        .sort((a, b) => {
          return ???
        }),
      tap(data => console.log('Community Cards: ', JSON.stringify(data))),
    );

Upvotes: 4

Views: 4508

Answers (3)

BizzyBob
BizzyBob

Reputation: 14750

You can achieve this by creating an enum to represent all possible values for CardCategory and defining the order they should be sorted in using Record<CardCategory, number>:

export enum CardCategory {
  OnGoing  = 'ONGOING',
  Upcoming = 'UPCOMING',
  Past     = 'PAST',
}

export const CategorySortOrder: Record<CardCategory, number> = {
  'ONGOING'  : 0,
  'UPCOMING' : 1,
  'PAST'     : 2
}

Then create a sort function that uses that to sort:

function sortByCardCategory(a: DisplayCard, b: DisplayCard) {  
  if(a.category === b.category) return 0;
  return CategorySortOrder[a.category] > CategorySortOrder[b.category] ? 1 : -1;
}

You can now easily sort like this:

displayCommunityCards$ = this.cardService.displayCards$.pipe(
  map(cards => cards
    .filter(c => c.type === CardType.CommunityEvent)
    .sort(sortByCardCategory)
  )
);

Here's a working StackBlitz demo.


Using the Record type isn't completely necessary, but it does guarantee that you define a sort order for all values of CardCategory. For example, if you added a new CardCategory, typescript will throw an error about CategorySortOrder not defining all values of CardCategory.

screenshot of error

Property 'MYNEWCAT' is missing in type
'{ 
  UPCOMING: number; 
  ONGOING: number; 
  PAST: number; 
}' 
but required in type 'Record<CardCategory, number>'. (2741)

Upvotes: 4

H3AR7B3A7
H3AR7B3A7

Reputation: 5261

While the other answer works, you don't need to create multiple arrays or even subscribe to achieve this. You are always better of just using async pipes in your template and providing the sorting function to use on the result of the async pipes.

The Enum & Interface used in this example:

enum Cat {
  ONGOING = 'ONGOING',
  UPCOMING = 'UPCOMING',
  PAST = 'PAST',
}

interface DisplayCommunityCards {
  category: Cat;
  type: string;
}

In your component:

cards$ = of([
  { category: Cat.ONGOING, type: 'COMMUNITY EVENT' },
  { category: Cat.PAST, type: 'COMMUNITY EVENT' },
  { category: Cat.PAST, type: 'COMMUNITY EVENT' },
  { category: Cat.UPCOMING, type: 'COMMUNITY EVENT' },
]);

displayCommunityCards$ = this.cards$.pipe(
  map((communityEventsCards) =>
    communityEventsCards.filter(
      (community) => community.type === 'COMMUNITY EVENT'
    )
  )
);

sort(
  displayCommunityCards: DisplayCommunityCards[]
): DisplayCommunityCards[] {
  const catOrder = Object.values(Cat);
  return displayCommunityCards.sort((a, b) => {
    return catOrder.indexOf(a.category) - catOrder.indexOf(b.category);
  });
}

(cards$ is just a replacement for the observable that you are getting from your service.)

In your template:

<p *ngFor="let c of sort(displayCommunityCards$ | async)" >
  {{ c.category }}
</p>

By doing it this way you will not have to worry about unsubscibing in an onDestroy and you are able to use the OnPush ChangeDetectionStrategy.

Here is an example on Stackblitz.

Upvotes: 2

Apoorva Chikara
Apoorva Chikara

Reputation: 8773

You can simply do this by creating three array and merge them in order. It can be done in O(n).

const upcoming = [],
      past = [],
      ongoing = [];
      
const events = [{event: "upcoming", id: 1}, {event: "upcoming", id: 2}, {event: "past", id: 3}, {event: "past", id: 4}, {event: "ongoing", id: 5}];

events.forEach(el => {
   el.event === "upcoming" ? upcoming.push(el) : 
   el.event === "past" ? past.push(el) :
   el.event === "ongoing" ? ongoing.push(el) : false;
});

console.log([...ongoing, ...upcoming, ...past]);

In the above snippet the memory used will be more.

I want to return an array that will sort like this: ONGOING --> UPCOMING --> PAST. So, all the display cards that have "ONGOING" will be in front, after there is no more of those, I want all the "UPCOMING" cards and then after those, I want all the "PAST" cards to show.

Another interesting way could be creating keys in an object with "ongoing", "upcoming" and "past". You can create an array as value for each and push things once you find it. You need to check when to start show the upcoming events and when to start past.

 const result = {
          upcoming: [],
          past: [],
          ongoing: [] 
 }
          
    const events = [{event: "upcoming", id: 1}, {event: "upcoming", id: 2}, {event: "past", id: 3}, {event: "past", id: 4}, {event: "ongoing", id: 5}];

    events.forEach(el => {
       el.event === "upcoming" ? result.upcoming.push(el) : 
       el.event === "past" ? result.past.push(el) :
       el.event === "ongoing" ? result.ongoing.push(el) : false;
    });

 console.log(result);

When you are done, just take the key and throw the data to the view.

Upvotes: 2

Related Questions