'Update Angular paginator after filtering?

I have a custom datasource in Angular where I have implemented sorting, paging and filtering. All of them works fine but when I go to filter the data in the table it shows me only the correspondence of the data present on the current page. The filter works on all data but the paginator does not update the table and does not show the appropriately reduced data.

This is the code

data model

import { Sistema } from './sistema'

export class Galassia {

  id: number;
  nome: string;
  imgLink: string;
  sistema: Sistema[];

}

service-rest.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { Galassia } from '../model/galassia';
import { catchError, tap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class ServizioProdottiRESTService {


  galassie: Galassia[];

  prodottiUrl = 'http://localhost:8080/prodotti';

  constructor(private http: HttpClient) { }

  getGalassie(): Observable<Galassia[]> {
    return this.http.get<Galassia[]>(this.prodottiUrl + '/galassie').pipe(
    tap(galaxy => this.galassie = galaxy),
    catchError(this.handleError('getGalassie', []))
  );
  }

  private handleError<T>(operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {
        console.log(error);
        this.log(`${operation} failed: ${error.message}`);
        return of(result as T);
    };
  }

  private log(message: string) {
    console.log('HeroService: ' + message);
  }
}

component.ts

import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTable } from '@angular/material/table';
import { ListaProdottiDataSource } from './lista-prodotti-datasource';
import { ServizioProdottiRESTService } from '../servizio-prodotti-rest.service';
import { Galassia } from '../../model/galassia';

@Component({
  selector: 'app-lista-prodotti',
  templateUrl: './lista-prodotti.component.html',
  styleUrls: ['./lista-prodotti.component.css']
})
export class ListaProdottiComponent implements AfterViewInit, OnInit {

  @ViewChild(MatPaginator) paginator: MatPaginator;
  @ViewChild(MatSort) sort: MatSort;
  @ViewChild(MatTable) table: MatTable<Galassia>;

  dataSource: ListaProdottiDataSource;
  displayedColumns = ['imgLink' ,'nome', 'button'];

  constructor(private myService: ServizioProdottiRESTService) {}

  ngOnInit() {
    this.dataSource = new ListaProdottiDataSource(this.myService);
  }

  ngAfterViewInit() {
    this.table.dataSource = this.dataSource;
    this.dataSource.sort = this.sort;
    this.dataSource.paginator = this.paginator;
  }

  applyFilter(event: Event) {
    const filterValue = (event.target as HTMLInputElement).value;
    this.dataSource.filter = filterValue.trim().toLowerCase();
  }
}

component.html

<div class="mat-elevation-z8 table">

  <table mat-table class="full-width-table" matSort aria-label="Elements" [dataSource]="dataSource.data">

    <!-- imgLink Column -->
    <ng-container matColumnDef="imgLink">
      <th mat-header-cell *matHeaderCellDef></th>
      <td mat-cell *matCellDef="let row"><img [src]="row.imgLink"/></td>
    </ng-container>

    <!-- Name Column -->
    <ng-container matColumnDef="nome">
      <th mat-header-cell *matHeaderCellDef mat-sort-header>Nome</th>
      <td mat-cell *matCellDef="let row">{{row.nome}}</td>
    </ng-container>

    <!-- Button Column -->
    <ng-container matColumnDef="button">
      <th mat-header-cell *matHeaderCellDef><div class="example-header">
        <mat-form-field>
          <input matInput (keyup)="applyFilter($event)" placeholder="Cosa vuoi cercare?">
        </mat-form-field>
      </div></th>
      <td mat-cell *matCellDef="let row"><img id="freccia" src="../../../assets/freccia.png"></td>
    </ng-container>

    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
  </table>

  <mat-paginator #paginator
      [length]="dataSource?.data.length"
      [pageIndex]="0"
      [pageSize]="5"
      [pageSizeOptions]="[5, 10, 15]">
  </mat-paginator>

</div>

and datasource.ts

import { DataSource } from '@angular/cdk/collections';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { map } from 'rxjs/operators';
import { Observable, of as observableOf, merge, BehaviorSubject } from 'rxjs';
import { ServizioProdottiRESTService } from '../servizio-prodotti-rest.service';
import { Galassia } from '../../model/galassia';

export class ListaProdottiDataSource extends DataSource<Galassia> {

  data: Galassia[];
  paginator: MatPaginator;
  sort: MatSort;

  get filter(): string { return this.filter$.getValue(); }
  set filter(value: string) { this.filter$.next(value); }
  filter$: BehaviorSubject<string> = new BehaviorSubject<string>('');

  constructor(private myService: ServizioProdottiRESTService) {
    super();
    this.myService.getGalassie().subscribe(galaxy => this.data = galaxy);
  }


  connect(): Observable<Galassia[]> {

    const dataMutations = [
      observableOf(this.data),
      this.paginator.page,
      this.sort.sortChange,
      this.filter$
    ];

    return merge(...dataMutations).pipe(map(() => {
      return this.getPagedData(this.getSortedData(this.getFilteredData([...this.data])));
    }));
  }

  disconnect() {}

  private getFilteredData(data: Galassia[]) {
    return data.filter(d => d.nome.toLowerCase().includes(this.filter));
  }


  private getPagedData(data: Galassia[]) {
    const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
    return data.splice(startIndex, this.paginator.pageSize);
  }

  private getSortedData(data: Galassia[]) {
    if (!this.sort.active || this.sort.direction === '') {
      return data;
    }

    return data.sort((a, b) => {
      const isAsc = this.sort.direction === 'asc';
      switch (this.sort.active) {
        case 'nome': return compare(a.nome, b.nome, isAsc);
        default: return 0;
      }
    });
  }
}

function compare(a: string | number, b: string | number, isAsc: boolean) {
  return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}

Some image of the issue

This is the first page before filtering

This is the second page before filtering

When I apply the filter on the first page referring to an element of the second page this happens

First page after filtering

The page is empty but if I go to the second page the element is displayed

Second page after filtering

As you can see also from the page indexes these are not updated because they are based on the complete array of elements and this is another problem



Solution 1:[1]

I solved the problem by declaring a temporary array containing the filtered data by initializing it in the constructor with the values of my main array and updating the paginator length in the getFilteredData method. In addition I changed the order of the calls to the methods in the connect () method

//declaration
filteredData: Galassia[];

constructor(private myService: ServizioProdottiRESTService) {
super();
this.myService.getGalassie().subscribe(galaxy => this.data = galaxy);

//Initialization
this.filteredData = this.data;
}

//Change the order of the calls
return this.getPagedData(this.getFilteredData(this.getSortedData([...this.data])));

//update getFilteredData method
private getFilteredData(data: Galassia[]) {
    this.filteredData = data.filter(d => d.nome.toLowerCase().includes(this.filter));
    this.paginator.length = this.filteredData.length;
    return this.filteredData;
}

I don't know if this is the best method but it works

Solution 2:[2]

Your original solution was correct, you just had to set the paginator's length before calling getPagedData. The updated connect method should be this:

  connect(): Observable<Galassia[]> {

    const dataMutations = [
      observableOf(this.data),
      this.paginator.page,
      this.sort.sortChange,
      this.filter$
    ];


    return merge(...dataMutations).pipe(
      map(() => this.getSortedData(this.getFilteredData([...this.data]))),
      tap(data => this.paginator.length = data.length),
      map(data => this.getPagedData(data))
    );
  }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 HAS
Solution 2 Leonardo Vidal