21

I want to sort a list of things by an observable field, but can't wrap my head around observables to get this working. Has somebody an idea how to achieve this?

The initial situation is something like the following:

Thing[] things;

interface Thing {
  name: Observable<string>
}
<ul>
  <li *ngFor="const thing for things">
    {{thing.name | async}}
  </li>
</ul>

Since I obviously haven't described my problem properly: The field I want to sort the list of things on is an Observable, not a plain string. I want to keep the field updated via websockets so to detect the changes properly I have to use an Observable field on which I can subscribe.

double-beep
  • 5,031
  • 17
  • 33
  • 41
sclausen
  • 1,720
  • 3
  • 15
  • 22

7 Answers7

29

Thanks for clarifying the question, Phosphoros. :)

Here's how you could do what you asked:

// Function to compare two objects by comparing their `unwrappedName` property.
const compareFn = (a, b) => {
  if (a.unwrappedName < b.unwrappedName)
    return -1;
  if (a.unwrappedName > b.unwrappedName)
    return 1;
  return 0;
};

// Array of Thing objects wrapped in an observable.
// NB. The `thing.name` property is itself an observable.
const thingsObs = Observable.from([
  { id: 1, name: Observable.of('foo') },
  { id: 2, name: Observable.of('bar') },
  { id: 3, name: Observable.of('jazz') }
]);

// Now transform and subscribe to the observable.
thingsObs

  // Unwrap `thing.name` for each object and store it under `thing.unwrappedName`.
  .mergeMap(thing =>
    thing.name.map(unwrappedName => Object.assign(thing, {unwrappedName: unwrappedName}))
  )

  // Gather all things in a SINGLE array to sort them.
  .toArray()

  // Sort the array of things by `unwrappedName`.
  .map(things => things.sort(compareFn))

  .subscribe();

Logging emitted values to the console will show an array of Thing objects sorted by their unwrappedName property:

[
  { id: 2, name: ScalarObservable, unwrappedName: "bar" },
  { id: 1, name: ScalarObservable, unwrappedName: "foo" },
  { id: 3, name: ScalarObservable, unwrappedName: "jazz" }
]

Please let me know if you have questions about this code.

AngularChef
  • 13,797
  • 8
  • 53
  • 69
  • Awesome, many thanks! This example didn't worked though (without throwing an error!), because compareFn must be declared before usage. – sclausen Feb 15 '17 at 14:16
  • 1
    You're welcome. I'll reorganize the code starting with the `compareFn` declaration. – AngularChef Feb 15 '17 at 14:28
  • @AngularChef I am faced with a very similar issue, but my sorting must be the result of the user clicking a button to sort the data already on the page. How would you sort this on a button click? – Chris22 Aug 24 '18 at 19:31
16

If I understand you correctly, you want to have an object that looks like this:

Thing {
   name: string;
}

You then have want to have an Observable that holds on array of Thing:

things$: Observable<Thing[]>;

You then want to sort your things in the thing array by a property, in this case name. That could be done like this:

...

let sorted$: Observable<Thing[]> = things$.map(items => items.sort(this.sortByName))

...

sortByName(a,b) {
  if (a.name < b.name)
    return -1;
  if (a.name > b.name)
    return 1;
  return 0;
}

...

And then finally, like Toung Le showed in his answer, change your template like this:

<ul>
  <li *ngFor="let thing of sorted$ | async">
    {{thing.name}} <!--No need async pipe here. -->
  </li>
</ul>
Fredrik Lundin
  • 8,006
  • 1
  • 29
  • 35
  • Many thanks for the extensive answer, but the name field I want to sort on isnt a string, but an Observable, since this field may be changed in the database and will then be updated automatically. – sclausen Feb 14 '17 at 21:36
  • @Phosphoros, hmm, that still sounds weird to me. Can you maybe update your question to show us how the data you are working on looks? – Fredrik Lundin Feb 15 '17 at 12:57
  • @FredrikLundin, (noob here) where do you put the code that you are declaring the method variable `sorted$`? In the `component.ts`? I want to call that method using a button click `` (if that's possible written as you have it). – Chris22 Aug 24 '18 at 20:01
5

You can use Observable.map. For example:

Observable<Thing[]> things;
sortedThings$ = things.map(items => items.sort()) // Use your own sort function here.

In your template:

<ul>
  <li *ngFor="let thing of sortedThings$ | async">
    {{thing.name}} <!--No need async pipe here. -->
  </li>
</ul>
Tuong Le
  • 18,533
  • 11
  • 50
  • 44
  • Unfortunately, this isn't helpful, since I'm interested in this nitty gritty detail, how to sort on an observable field. – sclausen Feb 13 '17 at 12:31
  • @Phosphoros. Your comment is not very clear. To me it looks like Tuong Le has answered exactly the question that you asked. Be more specific if you think his answer doesn't do the trick. – AngularChef Feb 13 '17 at 13:13
  • @AngularFrance "I want to sort a list of things by an observable field […]" isn't this clear enough? The observable field to sort on is the problem. – sclausen Feb 14 '17 at 07:06
  • 6
    Well, repeating the question verbatim doesn't make it any clearer. Also, keep in mind everyone on here is offering their time and knowledge to try to help you. If some of us don't understand the question as it is, it'll be more helpful to offer clarification even though it might already seem crystal clear to you. – AngularChef Feb 14 '17 at 08:41
  • Works like a charm. Thanks! Here's the link to docs on the sort function: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort – Otziii Mar 25 '18 at 18:13
3

You can use Observable.map then sort() with localeCompare which would look something like this :

.map(data => ({
        label: data.name
}))
.sort((a, b) => a.label.localeCompare(b.label));
Laurie Clark
  • 610
  • 7
  • 10
2

It is a pretty simple solution, once you understand how .pipe works and which Operators are there.

If we convert all items to an array using toArray operator and pipe the whole array as a single item to mergeMap operator, where we'll be able to sort it and then breaks out the array back.

interface Thing {
  name: string;
}

const things: Observable<Thing>;

...

things
   .pipe(toArray())
   .pipe(mergeMap(_things => 
      _things.sort((a, b) => 
         a.name.localeCompare(b.name)
      )
   ));

// OR

things.pipe(toArray(), mergeMap(_things => 
   _things.sort((a, b) => 
      a.name.localeCompare(b.name)
   )
));
Slavik Meltser
  • 9,712
  • 3
  • 47
  • 48
1

Use groupby operator (play with it):

const $things = getThings();

$things.pipe(
    groupBy(thing => thing.id),
    mergeMap(group$ => group$.pipe(
        reduce((acc, cur) =>[...acc, cur], [])
    ))
)
.subscribe(console.log)

Groupby docs.

Luillyfe
  • 6,183
  • 8
  • 36
  • 46
0

Changed the sort function with a filter function also made it work with rxjs 6.6.2

https://stackblitz.com/edit/rxjs-ys7a9m?file=index.ts

null canvas
  • 10,201
  • 2
  • 15
  • 18