Angular2 rxjs sort (observable) list of objects by an observable field

43,593

Solution 1

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.

Solution 2

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>

Solution 3

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>

Solution 4

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

Solution 5

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.

Share:
43,593
sclausen
Author by

sclausen

Updated on April 21, 2021

Comments

  • sclausen
    sclausen about 3 years

    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.