6

I'm trying to make my table columns sortable. I found this tutorial here: https://www.youtube.com/watch?v=UzRuerCoZ1E&t=715s

Using that information, I ended up with the following:

A pipe that handles the sorting

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

    @Pipe({
      name: 'sort',
      pure: true
    })
    export class TableSortPipe implements PipeTransform {
    
      transform(list: any[], column:string): any[] {
          let sortedArray = list.sort((a,b)=>{
            if(a[column] > b[column]){
              return 1;
            }
            if(a[column] < b[column]){
              return -1;
            }
            return 0;
          })
        return sortedArray;
      }
    
    }

Here's the component that helps me build my table. Here I define the sortedColumn variable.

import { NavbarService } from './../navbar/navbar.service';
import { LiveUpdatesService } from './live-updates.service';
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-live-updates',
  templateUrl: './live-updates.component.html',
  styleUrls: ['./sass/live-updates.component.scss']
})
export class LiveUpdatesComponent implements OnInit{
  stocks$: Observable<any[]>;
  sortedColumn: string;

  constructor(private updatesService: LiveUpdatesService, public nav: NavbarService) {
    this.stocks$ = this.updatesService.getStocks();
  }

  ngOnInit() {
    this.nav.show();
  }
}

Here's my template file. As you can see, I've attached my sort pipe to the my loop, spitting out the table rows. It's worth noting that the way I'm rendering the table differs from the video. For example, his data is stored in an array, but mine is stored on Firebase. He's rendering his table dynamically, but mine is fixed to a certain number of columns. I'm also hardcoding the headers, but he used the variable names from his array to generate the table headers. I'm not sure if these differences could be preventing things from working.

<section class="score-cards">
    <app-score-cards></app-score-cards>
</section>
<section class="live-updates-wrapper">
    <div class="table-wrapper">
        <table class="stock-updates">
            <thead>
                <tr>
                    <th class="ticker-fixed">Ticker</th>
                    <th><a (click)="sortedColumn = $any($event.target).textContent">Ask Price</a></th>
                    <th><a (click)="sortedColumn = $any($event.target).textContent">Tax Value</a></th>
                    <th><a (click)="sortedColumn = $any($event.target).textContent">Est. Value</a></th>
                    <th><a (click)="sortedColumn = $any($event.target).textContent">Location</a></th>
                </tr>
            </thead>
            <tbody>
                <tr *ngFor="let s of stocks$ | async | sort : sortedColumn">
                    <td class="ticker-fixed">
                        <a target="_blank" href="https://robinhood.com/stocks/{{ s.TICKER }}">{{ s.TICKER }}</a>
                        <span class="sp500">{{ s.sp500_flag }}S&P</span>
                    </td>
                    <td>{{ s.CLOSE }}</td>
                    <td>{{ s.tax_diff }}</td>
                    <td>{{ s.MarketCap }}</td>
                    <td>{{ s.Sector }}</td>
                </tr>
            </tbody>
        </table>
    </div>
</section>

I was getting the following error below, but was able to fix it injecting the following code in my pipe file: list = !!list ? list : [];

Now there are no errors, but the sorting is not working as expected. When I click on the table header, nothing happens. How can I fix this?

enter image description here

Kellen
  • 1,072
  • 2
  • 15
  • 32
  • What module is providing your SortPipe? – Z. Bagley Aug 26 '20 at 16:03
  • @Z. Bagley I don't know that I am using one. The tutorial didn't cover that. I apologize for my lack of knowledge. I know only a little Angular. – Kellen Aug 26 '20 at 16:24
  • This one covers it: https://stackoverflow.com/questions/40457744/angular2-custom-pipe-could-not-be-found/40463405 General idea: you need to declare the pipe in your `declarations` and then you also need to provide your pipe in your `providers`. If your only Module is AppModule, you can do both of those things there. – Z. Bagley Aug 26 '20 at 16:30
  • @Z.Bagley Ahh, yes! I used the ng g pipe command, so that all got added to my app.module.ts file automatically. It has been added to declarations, but not providers. Is providers necessary? I've implemented other pipes successfully that haven't been added to providers. – Kellen Aug 26 '20 at 16:39
  • 1
    It shouldn't be required in providers, you're not using it in a component. I think I see the actual issue though. Add before the first the line in your pipe: `list = !!list ? list : [];` The problem is that you're passing in `undefined` at some point, and it's trying to apply `.sort(..)` to `undefined`. The `list = !!list ? list : []` will apply an empty array to value any time it's undefined! – Z. Bagley Aug 26 '20 at 16:45
  • @Z.Bagley Awesome, that fixed the error! Sadly, my sorting still doesn't work. When I click the headers, nothing happens. I can create a separate ticket for that. – Kellen Aug 26 '20 at 16:51
  • throw that youtube tutorial in the trash. pipes for sorting is awful practice. – bryan60 Aug 28 '20 at 19:09
  • @bryan60 I'm open to alternative solutions. – Kellen Aug 28 '20 at 19:10

3 Answers3

11

forget the pipe. sorting via pipe is bad practice, leads to either buggy code or bad performance.

Use observables instead.

first change your template header buttons to call a function, and also make sure you're feeding the actual property names you want to sort by, rather than the header content:

<th><a (click)="sortOn('CLOSE')">Ask Price</a></th>
<th><a (click)="sortOn('tax_diff')">Tax Value</a></th>
<th><a (click)="sortOn('MarketCap')">Est. Value</a></th>
<th><a (click)="sortOn('Sector')">Location</a></th>

then, pull out your sort function and import to your component:

  export function sortByColumn(list: any[] | undefined, column:string, direction = 'desc'): any[] {
      let sortedArray = (list || []).sort((a,b)=>{
        if(a[column] > b[column]){
          return (direction === 'desc') ? 1 : -1;
        }
        if(a[column] < b[column]){
          return (direction === 'desc') ? -1 : 1;
        }
        return 0;
      })
    return sortedArray;
  }

then fix up your component:

// rx imports
import { combineLatest, BehaviorSubject } from 'rxjs';
import { map, scan } from 'rxjs/operators';

...

export class LiveUpdatesComponent implements OnInit{
  stocks$: Observable<any[]>;
  // make this a behavior subject instead
  sortedColumn$ = new BehaviorSubject<string>('');
  
  // the scan operator will let you keep track of the sort direction
  sortDirection$ = this.sortedColumn$.pipe(
    scan<string, {col: string, dir: string}>((sort, val) => {
      return sort.col === val
        ? { col: val, dir: sort.dir === 'desc' ? 'asc' : 'desc' }
        : { col: val, dir: 'desc' }
    }, {dir: 'desc', col: ''})
  )

  constructor(private updatesService: LiveUpdatesService, public nav: NavbarService) {
    // combine observables, use map operator to sort
    this.stocks$ = combineLatest(this.updatesService.getStocks(), this.sortDirection$).pipe(
      map(([list, sort]) => !sort.col ? list : sortByColumn(list, sort.col, sort.dir))
    );
  }

  // add this function to trigger subject
  sortOn(column: string) {
    this.sortedColumn$.next(column);
  }

  ngOnInit() {
    this.nav.show();
  }
}

finally, fix your ngFor:

<tr *ngFor="let s of stocks$ | async">

this way, you're not relying on magic or change detection. you're triggering your sort when it needs to trigger via observables

bryan60
  • 28,215
  • 4
  • 48
  • 65
  • Would you put the sorting function in a service? – Kellen Aug 28 '20 at 19:31
  • 1
    i wouldn't bother, as it has no dependencies and no state. it's a prime candidate for a `utlities/array.functions.ts` file ... or you could just add lodash to your project and use their `sortBy` function – bryan60 Aug 28 '20 at 19:32
  • Thanks for your help. So, I've gotten everything in place, but I'm getting an error in the console, and the sorting is not happening. I'll post the error below. – Kellen Aug 28 '20 at 19:51
  • `ERROR TypeError: Cannot read property 'list' of undefined at sortByColumn (array.functions.ts:11) at MapSubscriber.project (live-updates.component.ts:22) at MapSubscriber._next (map.js:29) at MapSubscriber.next (Subscriber.js:49) at CombineLatestSubscriber.notifyNext (combineLatest.js:73) at InnerSubscriber._next (InnerSubscriber.js:11) at InnerSubscriber.next (Subscriber.js:49) at BehaviorSubject.next (Subject.js:39) at BehaviorSubject.next (BehaviorSubject.js:30) at LiveUpdatesComponent.sortOn (live-updates.component.ts:26)` – Kellen Aug 28 '20 at 19:52
  • for some reason the sort function is being fed `undefined` instead of an array... does your `getStocks()` method emit undefined for some reason? either way, i modified the sort function to expect and handle undefined cases. – bryan60 Aug 28 '20 at 19:54
  • also, make sure you have all my changes and latest updates. i made some edits after the initial. – bryan60 Aug 28 '20 at 19:59
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/220636/discussion-between-kellen-and-bryan60). – Kellen Aug 28 '20 at 20:07
2

I think your values aren't passed on into the pipe:

Can you try:

<tr *ngFor="let s of ((stocks$ | async) | sort : sortedColumn)">
Ondie
  • 151
  • 5
  • 1
    You could swap the pipes, instead of your custome pipe recieving an array you could recieve the opservable which you return and do your filtering on and then async pipe it – Ondie Aug 26 '20 at 21:53
  • I'm not entirely certain how to accomplish that. I'm kind of an Angular noob. Nonetheless, it looks like the data is flowing into the pipe. I'm seeing data when run console.log(column); and console.log(list); in my pipe. But the order of my table isn't updating. I'm wondering if it's how I'm rendering the table. There are some differences between my table and the one used in the tutorial. – Kellen Aug 27 '20 at 11:46
1

Asyn call overhere before assigning the value this.stocks$ table will load pipe will called

 constructor(private updatesService: LiveUpdatesService, public nav: NavbarService) {
    this.stocks$ = this.updatesService.getStocks();
  }

Template

 <tbody *ngIf="stocks$">
       <tr *ngFor="let s of stocks$ | sort : sortedColumn">
          ....
        </tr>
  </tbody>
karthicvel
  • 343
  • 2
  • 5