1

First of all, I'm French so all my apologies for my english...

I would like to order an Observable with *ngFor.

Here is my code :

this.allBoulangeries$ = this.allBoulangeriesService.getAllBoulangeries();

I get all datas from firebase.

Next, I do a *ngFor to display :

<ion-card *ngFor="let boulangerie of allBoulangeries$ | async"  tappable (click)="viewDetails(boulangerie)">
        <div padding>
          <b>{{ boulangerie.name }}</b>
          <p>{{ boulangerie.address }}<br>
            {{ boulangerie.cp }} {{ boulangerie.ville }}</p>
          <p>Distance: {{ distanceTo(boulangerie.coordX, boulangerie.coordY, latitude, longitude) }}</p>
        </div>
      </ion-card>

My problem that is I want to order the list with the distance value (calculate from coordX and coordY with a function)... For the moment, the list is displayed in alphabetic order.

Thank you very much for your support !

Jota.Toledo
  • 27,293
  • 11
  • 59
  • 73
  • You can implement a custom pipe that does ordering or use [this pipe](https://github.com/fknop/angular-pipes/blob/master/docs/array.md#orderby). Also you can see: https://stackoverflow.com/questions/35158817/angular-2-orderby-pipe – Harun Yilmaz Feb 05 '19 at 11:03
  • @HarunYılmaz it isnt as simple as that, read the question again – Jota.Toledo Feb 05 '19 at 11:08
  • So there a couple of things wort mentioning: First, `ngForOf` **does not sorts an iterable**, it simply iterates it. The best approach would be to pass an already sorted list to it. Second, you are binding a function call to your template, which will be executed a lot of times. Depending on the size of the list and how complex is that calculation (most likely norm2 distance?), it could reduce the app performance. – Jota.Toledo Feb 05 '19 at 11:17
  • Another factor would be to know, do the values of `latitude` and `longitude` change during the lifetime of the component where they exist? If so, how are you binding them to their respective values? – Jota.Toledo Feb 05 '19 at 11:19

3 Answers3

0

You could create a custom pipe to calculate and order by distance:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({name: 'orderByDistance'})
export class OrderByDistancePipe implements PipeTransform {
  transform(boulangeries: Boulangerie[], latitude: number, longitude: number): Boulangerie[] {
    return boulangeries.sort((a, b) => {
        return distanceTo(b.coordX, b.coordY, latitude, longitude) -
               distanceTo(a.coordX, a.coordY, latitude, longitude);
    });
  }
}

This pipe returns your ordered list of boulangeries. Then you can use it on your array by passing the array before the pipe, and giving the latitude and longitude arguments after :.

<ion-card *ngFor="let boulangerie of ((allBoulangeries$ | async) | orderByDistance:latitude:longitude)"  tappable (click)="viewDetails(boulangerie)">
    <div padding>
        <b>{{ boulangerie.name }}</b>
        <p>{{ boulangerie.address }}<br>
           {{ boulangerie.cp }} {{ boulangerie.ville }}</p>
        <p>Distance: {{ distanceTo(boulangerie.coordX, boulangerie.coordY, latitude, longitude) }}</p>
    </div>
</ion-card>

Another way to do it is to sort your array in your component, by piping to your observable to return an observable of your ordered array:

this.boulangeriesOrdered$ = this.boulangeries.pipe(
    map(arr => arr.sort((a, b) =>
        distanceTo(b.coordX, b.coordY, latitude, longitude) -
        distanceTo(a.coordX, a.coordY, latitude, longitude)
    ))
);

You can then use this observable in place of boulangeries$.

Performance-wise, both approaches will be comparable since the ordering will hapen every time the observable will generate a new array.

jo_va
  • 13,504
  • 3
  • 23
  • 47
0

The code that you've written is extremely costly as far as performance is concerned as you're calling a function in the string interpolation syntax({{}})

I'd recommend that you do that in the Component itself. Give this a try:

this.allBoulangeries$ = this.allBoulangeriesService.getAllBoulangeries()
  .pipe(
    map(boulangeries => boulangeries.map(boulangerie => {
      return { 
        ...boulangerie, 
        distance: this.distanceTo(boulangerie.coordX, boulangerie.coordY, this.latitude, this.longitude) 
      };
    })),
    map(boulangeries => boulangeries.sort((b1, b2) => b1.distance - b2.distance ))
  );
SiddAjmera
  • 38,129
  • 5
  • 72
  • 110
0

Solution 1: Assuming you have access to long and lat when you are pushing these objects to firebase

When you push a boulangerie to Firebase add the calculated distance to the object before you push it to the database. This is more efficient than doing this for each object when you get the collection of boulangeries.

When you query the collection of boulangeries use orderBy('distance') on your call to the database (assuming your firebase database dependency is called db):

If using the realtime database the method in your BoulangerieService it would look like:


getAll(): Observable<Boulangerie[]> {
    db.collection('items', ref => ref.orderBy('distance')).valueChanges();
}

In Firestore it would look like this:


getAll(): Observable<Boulangerie[]> {
    this.db.list('/boulangeries', ref => ref.orderByChild('distance')).valueChanges()
}

Then if you need to sort by alphabetical order or by any other property you could adapt one of the pipes provided in the other answers to do this quite nicely :)

Solution 2: If you don't have access to the long and lat when pushing to firebase

I strongly suggest that you do the following in a service and not the component - this is a better practice because you are separating the manipulating of data from the presentation of it in the component. This is also the better way to do it using RxJS. Note you will need to import the map operator from RxJS to do the following.

Make sure that the longitude and latitude are available, and your distanceTo() function, in the service or component where you are making the call to firebase and try:


// If using firestore:

getAll(): Observable<Boulangerie[]> {
    this.db.list('/boulangeries', ref => ref.orderByChild('distance')).valueChanges()
    .pipe(
        map((boulangeries: Boulangerie[]) => {
            boulanderies.forEach((boulangerie) => {
                boulangerie.distance = distanceTo(boulangerie.coordX, boulangerie.coordY, latitude, longitude)
            })
        })
    )
}

nclarx
  • 847
  • 1
  • 8
  • 18