简体   繁体   中英

Reactive Angular Material Data Table

I've created an Angular Material Data Table with this ng generate @angular/material:material-table command and it gave me following file structure:

  • table-datasource.ts
  • table.component.ts
  • table.component.html

The idea here is to do all the fetching, sorting and pagination in the table-datasource.ts . By default the data is placed in an Array inside table-datasource.ts but in my case its coming from an ngxs-store which exposes an Observable of an Array . Atm I have following implementation:

table-datasource.ts:

export class TokenTableDataSource extends DataSource<TokenTableItem> {
  @Select(TokenTableState.getTokenTableItems) private tokenTableItems$:Observable<TokenTableItem[]>;
  totalItems$ = new BehaviorSubject<TokenTableItem[]>([]);

  constructor(private paginator: MatPaginator, private sort: MatSort) {
    super();
  }

  /**
  * Connect this data source to the table. The table will only update when
  * the returned stream emits new items.
  * @returns A stream of the items to be rendered.
  */
  connect(): Observable<TokenTableItem[]> {
    this.tokenTableItems$.subscribe(item => this.totalItems$.next(item));

    // init on first connect
    if (this.totalItems$.value === undefined) {
      this.totalItems$.next([]);
      this.paginator.length = this.totalItems$.value.length;
    }
    // Combine everything that affects the rendered data into one update
    // stream for the data-table to consume.
    const dataMutations = [
      observableOf(this.totalItems$),
      this.paginator.page,
      this.sort.sortChange
    ];

    return merge(...dataMutations).pipe(
      map(() =>  this.totalItems$.next(this.getPagedData(this.getSortedData([...this.totalItems$.value])))),
      mergeMap(() => this.totalItems$)
    );
  }
  ...generated paging and sorting methods

table-component.html:

<div class="mat-elevation-z8">
  <table mat-table class="full-width-table" [dataSource]="dataSource" matSort aria-label="Elements">

  ...multiple columns

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

  <mat-paginator #paginator
      [length]="this.dataSource.totalItems$.value?.length"
      [pageIndex]="pageIndex"
      [pageSize]="pageSize"
      [pageSizeOptions]="pageSizeOptions"
      [showFirstLastButtons]=true
      (page)="handlePage($event)">
  </mat-paginator>
</div>

table.component.ts:

export class TokenTableComponent implements OnInit {
  @ViewChild(MatPaginator) paginator: MatPaginator;
  @ViewChild(MatSort) sort: MatSort;
  dataSource: TokenTableDataSource;

  pageSizeOptions = [5, 10, 20, 40];
  pageSize = this.pageSizeOptions[0];
  pageIndex = 0;
  tableLength = 0;

  ... colums definition

  ngOnInit(): void {
    this.dataSource = new TokenTableDataSource(this.paginator, this.sort);
  }

  public handlePage(pageEvent: PageEvent) {
    // do what?
  }
}

What's working:

  • The data is rendered correct (triggered with a button and via the ngxs-store)
  • I can sort the data

What's not working:

  • On first data load the pageSize is ignored at all and all rows are displyed
  • When clicking sorting or a pagination element, the current selected pageSize is taken and this amount of rows is rendered. What's strange to me is that this only works descending (given pageSize is 10 and I select 5 it results in 5 rows but once 5 is selected it's not possible to display more rows than 5 again)

Requirements:

  • I like the idea to encapsulate all data manipulations behind TableDataSource.connect() so a solution like this where the fetching is done in the comonent is not desired. Furthermore this doesn't have sorting implemented.
  • The app uses an ngxs-store , which is very similar to ngrx , so any solution involving this part is welcome.
  • I haven't figured out what to do with pageEvents so my guess is that the solution is in the handlePage() method.

Versions:

  • RxJS 6.3.x
  • Angular 7.x
  • ngxs 3.3.x

I figured out how to setup a table for my requirements. The main change is that I removed the Observable which fetches the data from the TableDataSource and introduced a DataService :

export class DataService {
  //the @Select is from ngxs but can be anything returning an Observable 
  @Select(TokenTableState.getTokenTableItems) private tokenTableItems$: Observable<TokenTableViewItem[]>;
  private initValue$: BehaviorSubject<TokenTableViewItem[]> = new BehaviorSubject<TokenTableViewItem[]>([]);

  getAllItems(): Observable<TokenTableViewItem[]> {
    const sources = [this.tokenTableItems$, this.initValue$];
    return merge(...sources);
  }
}

Basically that service gets the data from any Observable input and merges this in the getAllItems method with an initial value.

The Component has an instance of this service:

private _dataService: DataService | null;

which it hands over to the TableDatasource in the load method:

private loadData(): any {
    this._dataService = new DataService();
    this.dataSource = new TokenTableDataSource(
      this._dataService,
      this.paginator,
      this.sort
    );
    fromEvent(this.filter.nativeElement, 'keyup').subscribe(() => {
      if (!this.dataSource) {
        return;
      }
      this.dataSource.filter = this.filter.nativeElement.value;
    });
  }

The reason I don't have a reference of the DataService in the TableDataSource is that the paginator in the Component needs the length of the table for rendering (seen below).

The TableDataSource consumes the DataService like this:

  • In the connect method it holds an array with possible data mutations:

     const dataMutations = [ this._dataChange, this._sort.sortChange, this._filterChange, this._paginator.page ]; 
  • The _dataChange member of the array gets it value by subscribing to the getAllItems method from our DataService :

     this._internalService.getAllItems().subscribe(data => { this._dataChange.next(data); }); 
  • The dataMutations are used like this to filter, sort and return the data which should be displayed:

     return merge(...dataMutations).pipe( map(() => { // Filter data this.filteredData = this._dataChange.value .slice() .filter((item: TokenTableViewItem) => { const searchStr = (item.symbol + item.name).toLowerCase(); return searchStr.indexOf(this.filter.toLowerCase()) !== -1; }); // Sort filtered data const sortedData = this.getSortedData(this.filteredData.slice()); // Grab the page's slice of the filtered sorted data. this.renderedData = this.getPagedData(sortedData); return this.renderedData; }) ); 

The filterChange is defined in the local instance

_filterChange = new BehaviorSubject('');

while the pagination and sorting are triggered from outside via the constructor

constructor(
  public _internalService: DataService,
  public _paginator: MatPaginator,
  public _sort: MatSort
) {
  super();
  this._filterChange.subscribe(() => (this._paginator.pageIndex = 0));
}

I also found a solution for the pagination which is defined in the component.html like this:

<mat-paginator #paginator
  [length]="dataSource.filteredData.length"
  [pageIndex]="pageIndex"
  [pageSize]="pageSize"
  [pageSizeOptions]="pageSizeOptions"
  [showFirstLastButtons]=true>
</mat-paginator>

and with the variables set in the component.ts :

pageSizeOptions = [5, 10, 20, 40];
pageSize = this.pageSizeOptions[0];
pageIndex = 0;

The full code can be seen at this project and a live version of the table is used at whatsmytoken.com .

WOW!
just about the same time, I wrote an article about my Reactive DataSource, that can be easily extended for multiple data lists! you can add optional and required mutators , accompanied of getter functions to collect the respective arguments and merge them in a REQuest object.

I explained the overall stuff here:
https://medium.com/@matheo/reactive-datasource-for-angular-1d869b0155f6

and I mounted a demo on StackBlitz too with a Github repo showing with simple commits, how simple is to set up a filtered/sorted/paginated list in a clean way.

I hope you give me some feedback about my library,
and if you find it appealing, I can be sure to support you with your use cases too :)

Happy coding!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM