33
loading...
This website collects cookies to deliver better user experience
Have on it server side pagination.
The parameters provided by the API in this case for pagination include a pageSize and a pageIndex. For example, appending a pageSize of 5 and a pageIndex of 1 to the URL as query string means 5 users will be spooled for the first page.
The URL suffix should look something like this. .../users?pageSize=5&pageIndex=1
A search parameter to filter the entire records of users based on specified search input typed in by the user. For this, an input field is to be provided on top of the table to allow users type in their search query. e.g. typing in brosAY should bring in all the users related to brosAY.
The URL suffix should look something like this .../users?pageSize=5&pageIndex=1&searchString=brosAY
Have a loader that shows anytime we are making an API call to retrieve new set of users. Mostly when the previous or back button is pressed.
//SEARCH FORM CONTROL
<mat-form-field appearance="fill">
<mat-label>Input your search text</mat-label>
<input matInput placeholder="Search" [formControl]="searchInput">
<button mat-icon-button matPrefix>
<mat-icon>search</mat-icon>
</button>
</mat-form-field>
//USERS TABLE
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> User ID. </th>
<td mat-cell *matCellDef="let user"> {{element.id}} </td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Name </th>
<td mat-cell *matCellDef="let user"> {{user.name}} </td>
</ng-container>
<ng-container matColumnDef="age">
<th mat-header-cell *matHeaderCellDef> Age </th>
<td mat-cell *matCellDef="let user"> {{user.age}} </td>
</ng-container>
<ng-container matColumnDef="address">
<th mat-header-cell *matHeaderCellDef> Address </th>
<td mat-cell *matCellDef="let user"> {{user.address}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<!-- Mat Paginator -->
<mat-paginator (page)="onPageChange($event)" [length]="dataLength" [pageSizeOptions]="[5, 10, 20, 50, 100]" showFirstLastButtons></mat-paginator>
</div>
displayedColumns: string[] = [
'id',
'name',
'age',
'address',
];
//Form Control for search inputs on the table
searchInput = new FormControl();
//<User> represents the User Model
dataSource = new MatTableDataSource<User>();
//Inject the UserService
constructor(public userService: UserService){}
<!-- Mat Paginator -->
<mat-paginator (page)="onPageChange($event)" [length]="dataLength" [pageSizeOptions]="[5, 10, 20, 50, 100]" showFirstLastButtons></mat-paginator>
</div>
constructor(public userService: UserService){ }
// we initialize the pageIndex to 1 and pageSize to 5
pageIndex: number = 1;
pageSize: number = 5;
//this method receives the PageEvent and updates the pagination Subject.
onPageChange = (event: PageEvent): void => {
// the current page Index is passed to the pageIndex variable
this.pageIndex = event.pageIndex;
// the current page Size is passed to the pageSize variable
this.pageSize = event.pageSize;
/**the pagination method within the user service is called and the
current pagination passed to it**/
this.userService.updatePagination({
pageIndex: this.pageIndex,
pageSize: this.pageSize
})
}
export interface Pagination {
pageIndex: number,
pageSize: number
}
/** <Pagination> stands as the BehaviorSubject's model which means that any value that will be assigned to the behaviorSubject must conform to the Pagination model. **/
/** within the () is where we specify the default value for our pagination which is pageSize of 5 and pageIndex of 1 in this case.**/
private paginationSubject = new BehaviorSubject<Pagination>({
pageIndex: 1;
pageSize: 5;
});
/** <string> below as usual, stands for the data type of the value that is allowed to be passed into the subject.
**/
private searchStringSubject = new BehaviorSubject<string>(null);
//Form Control for search inputs on the table
searchInput = new FormControl();
constructor(public userService: UserService){}
ngOnInit(){
this.trackSearchInput();
}
//method triggers when the search Form Control value changes.
// the changed value doesnt get passed on until after .8s
trackSearchInput = (): void => {
this.searchInput.valueChanges.pipe(debounceTime(800)).subscribe((searchWord: string) => this.userService.updateSearchStringSubject(searchWord))
}
/** this method is the only single point where the pagination subject can be updated. **/
updatePaginationSubject = (pagination: Pagination): void => {
this.paginationSubject.next(pagination);
}
/** Likewise, this method is the only single point where the search string subject can be updated.
**/
updateSearchStringSubject = (searchString: string): void => {
this.searchStringSubject.next(searchString);
}
private paginationSubject = new BehaviorSubject<Pagination>({
pageSize: 5;
pageIndex: 1;
});
//below convert the pagination BehaviorSubject to an observable
public pagination$ = this.paginationSubject.asObservable();
private searchStringSubject = new BehaviorSubject<string>(null);
searchString$ = this.searchStringSubject.asObservable();
paginatedUsers$ = combineLatest([
this.pagination$,
this.searchString$.pipe(startWith(null)) /**starts with an empty string.**/
])
/**However, because we already have a default state of null for the search string we have this**/
paginatedUsers$ = combineLatest([
this.pagination$,
this.searchString$
])
baseUrl = "https://www.wearecedars.com";
paginatedUsers$: Observable<PagedUsers> = combineLatest([
this.pagination$,
this.searchString$
]).pipe(
/**[pagination - stands for the pagination object updated on page change]
searchString stands for the search input
**/
switchMap(([pagination, searchString]) =>
this.http.get<ApiResponse<PagedUsers>>(`${this.baseUrl}/users?
pageSize=${pagination.pageSize}&pageIndex=${pagination.pageIndex}
${searchString ? '&searchInput=' + searchString : ''}`).pipe(
map(response => response?.Result)
))
).pipe(shareReplay(1))
/**shareReplay(1) is applied in this case because I want the most recent response cached and replayed among all subscribers that subscribes to the paginatedUsers$. (1) within the shareReplay(1) stands for the bufferSize which is the number of instance of the cached data I want replayed across subscribers.**/
constructor(public userService: UserService){}
//the pagedUsers$ below is subscribed to on the template via async pipe
pagedUsers$ = this.userService.paginatedUsers$.pipe(
tap(res=> {
//update the dataSource with the list of allusers
this.dataSource.data = res.allUsers;
/**updates the entire length of the users. search as the upper bound for the pagination.**/
this.dataLength = res.totalElements
})
)
<ng-container *ngIf="pagedUsers$ | async as pagedUsers">
<mat-form-field appearance="fill">
<mat-label>Input your search text</mat-label>
<input matInput placeholder="Search" [formControl]="searchInput">
<button mat-icon-button matPrefix>
<mat-icon>search</mat-icon>
</button>
</mat-form-field>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> User ID. </th>
<td mat-cell *matCellDef="let user"> {{element.id}} </td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Name </th>
<td mat-cell *matCellDef="let user"> {{user.name}} </td>
</ng-container>
<ng-container matColumnDef="age">
<th mat-header-cell *matHeaderCellDef> Age </th>
<td mat-cell *matCellDef="let user"> {{user.age}} </td>
</ng-container>
<ng-container matColumnDef="address">
<th mat-header-cell *matHeaderCellDef> Address </th>
<td mat-cell *matCellDef="let user"> {{user.address}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<!-- Mat Paginator -->
<mat-paginator (page)="onPageChange($event)" [pageSize]="pagedUsers?.pageable?.pageSize"
[pageIndex]="pageIndex"
[length]="dataLength" [pageSizeOptions]="[5, 10, 20, 500, 100]" showFirstLastButtons></mat-paginator>
</div>
</ng-container>
subscribe to the loader observable on the template in such a way that the loader shows only when the loader observavle is true.
As soon as the previous, next button is clicked or value is entered for the pagination, the onPageChange method is triggered. before calling the updatePaginationSubject we call the method that sets the loader B-Subject to true. Then as soon as response is returned from the API call to get users, we set the loader subject back to false.
// we initialize the pageIndex to 1 and pageSize to 5
pageIndex: number = 1;
pageSize: number = 5;
onPageChange = (event: PageEvent): void => {
/** set the loader to true; immediately the loader starts showing on
the page **/
this.userService.showLoader();
// the current page Index is passed to the pageIndex variable
this.pageIndex = event.pageIndex;
// the current page Size is passed to the pageSize variable
this.pageSize = event.pageSize;
this.userService.updatePagination({
pageIndex: this.pageIndex,
pageSize: this.pageSize
})
}
/**<boolean> is used as data type because the loading status can either be true or false**/
private loaderSubject = new BehaviorSubject<boolean>(false);
public loading$ = this.loaderSubject.asObservable();
//method sets the loader to true basically
showLoader = (): void => {
this.loaderSubject.next(true);
};
//method sets the loader to false
hideLoader = (): void => {
this.loaderSubject.next(false);
}
/**<boolean> is used as data type because the loading status can either be true or false**/
private loaderSubject = new BehaviorSubject<boolean>(false);
public loading$ = this.loaderSubject.asObservable();
// method sets the loader to true
showLoader = (): void => {
this.loaderSubject.next(true);
};
// method sets the loader to false;
hideLoader = (): void => {
this.loaderSubject.next(false);
}
paginatedUsers$ = combineLatest([
this.pagination$,
this.searchString$
]).pipe(
switchMap(([pagination, searchString]) =>
this.http.get<ApiResponse<PagedUsers>>(`${this.baseUrl}/users?
pageSize=${pagination.pageSize}&pageIndex=${pagination.pageIndex}&
${searchString ? '&searchInput=' + searchString : ''}`).pipe(
// The actual response result is returned here within the map
map((response) => response?.Result),
/** within the tap operator we hide the Loader. Taps are mostly used for side-effects like hiding loaders while map is used mostly to modify the returned data **/
tap(() => this.hideLoader()),
/** we use the catchError rxjs operator for catching any API errors but for now we will mainly return EMPTY. Mostly, Interceptors are implemented to handle server errors.**/
catchError(err => EMPTY),
/**A finally is implemented to ensure the loader stops no matter. You can have the loader hidden only within the finally operator since the method will always be triggered**/
finally(() => this.hideLoader());
))
).pipe(shareReplay(1))
<ng-container *ngIf="pagedUsers$ | async as pagedUsers">
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> User ID. </th>
<td mat-cell *matCellDef="let user"> {{element.id}} </td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Name </th>
<td mat-cell *matCellDef="let user"> {{user.name}} </td>
</ng-container>
...
</ng-container>
// the loader displays on top of the page when loading...
<app-loader *ngIf="userService.loading$ | async"></app-loader>
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatPaginator) set matPaginator(mp: MatPaginator) {
this.paginator = mp;
}
displayedColumns: string[] = [
'id',
'name',
'age',
'address',
];
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatPaginator) set matPaginator(mp: MatPaginator) {
this.paginator = mp;
}
pageIndex: number = 1;
pageSize: number = 5;
searchInput = new FormControl();
dataSource = new MatTableDataSource<User>();
pagedUsers$ = this.userService.paginatedUsers$.pipe(
tap(res=> {
this.dataSource.data = res.allUsers;
this.dataLength = res.totalElements
}
))
ngOnInit(){
this.trackSearchInput();
}
trackSearchInput = (): void => {
this.searchInput.valueChanges.pipe(debounceTime(800)).subscribe(
(searchWord: string) => this.userService.updateSearchStringSubject(searchWord))
}
constructor(public userService: UserService) { }
onPageChange = (event: PageEvent): void => {
this.userService.showLoader();
this.pageIndex = event.pageIndex;
this.pageSize = event.pageSize;
this.userService.updatePagination({
pageIndex: this.pageIndex,
pageSize: this.pageSize
})
}
<ng-container *ngIf="pagedUsers$ | async as pagedUsers">
<mat-form-field appearance="fill">
<mat-label>Input your search text</mat-label>
<input matInput placeholder="Search" [formControl]="searchInput">
<button mat-icon-button matPrefix>
<mat-icon>search</mat-icon>
</button>
</mat-form-field>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> User ID. </th>
<td mat-cell *matCellDef="let user"> {{element.id}} </td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Name </th>
<td mat-cell *matCellDef="let user"> {{user.name}} </td>
</ng-container>
<ng-container matColumnDef="age">
<th mat-header-cell *matHeaderCellDef> Age </th>
<td mat-cell *matCellDef="let user"> {{user.age}} </td>
</ng-container>
<ng-container matColumnDef="address">
<th mat-header-cell *matHeaderCellDef> Address </th>
<td mat-cell *matCellDef="let user"> {{user.address}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<!-- Mat Paginator -->
<mat-paginator (page)="onPageChange($event)" [length]="dataLength" [pageSizeOptions]="[5, 10, 20, 50, 100]" showFirstLastButtons></mat-paginator>
</div>
<ng-container>
<app-loader *ngIf="userService.loading$ | async"></app-loader>
//pagination Subject
private paginationSubject = new BehaviorSubject<Pagination>({
pageIndex: 1;
pageSize: 5;
});
//pagination Observable
public pagination$ = this.paginationSubject.asObservable();
//Search string Subject
private searchStringSubject = new BehaviorSubject<string>();
//Search string Observable
public searchString$ = this.searchStringSubject.asObservable();
//Loader subject
private loaderSubject = new BehaviorSubject<boolean>(false);
//Loading observable
public loading$ = this.loaderSubject.asObservable();
/** baseUrl for the users endpoint. In real life cases test URLs should be in the environment.ts while production Urls should be in the environment.prod.ts **/
baseUrl = "https://www.wearecedars.com";
//returns all Paginated Users
paginatedUsers$ = combineLatest([
this.pagination$,
this.searchString$
]).pipe(
switchMap(([pagination, searchString]) =>
this.http.get<ApiResponse<PagedUsers>>(`${this.baseUrl}/users?
pageSize=${pagination.pageSize}&pageIndex=${pagination.pageIndex}&
${searchString ? '&searchInput=' + searchString : ''}`).pipe(
map((response) => response?.Result),
tap(() => this.hideLoader()),
catchError(err => EMPTY),
finally(() => this.hideLoader())
))
).pipe(shareReplay(1))
//Method updates pagination Subject
updatePaginationSubject = (pagination: Pagination): void => {
this.paginationSubject.next(pagination)
}
//Method updates search string Subject
updateSearchStringSubject = (searchString: string): void => {
this.searchStringSubject.next(searchString)
}
//Method sets loader to true
showLoader = (): void => {
this.loaderSubject.next(true);
};
//Method sets loader to false
hideLoader = (): void => {
this.loaderSubject.next(false);
}
export interface Pagination {
pageIndex: number,
pageSize: number
}
export interface APIResponse<T> {
TotalResults: number;
Timestamp: string;
Status: string;
Version: string;
StatusCode: number;
Result: T;
ErrorMessage?: string;
}
export interface PagedUsers {
allUsers: AllUsers[];
totalElements: number;
...
}
export interface AllUsers {
id: number;
name: string;
age: number;
address: string;
}