I've created an Angular Material Data Table with this ng generate @angular/material:material-table
command and it gave me following file structure:
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:
What's not working:
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:
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. handlePage()
method. Versions:
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.