Angular Search & Pagination

with usage examples

Leonardo Giroto
ITNEXT

--

In a real world application its really common to work with big amounts of data and to present the user with the possibility to search through it and present it paginated. Therefore, in this article, I’ll show one approach on how to create a search component that handles search inputs with debounce and a pagination component which will handle requesting different pages of data; both working together.

Search Component

Let’s start with our Search Component. Our goal here is to provide an input to the user so he can type a search string to filter the results on a big collection. However, we must notice that by using direct property binding we do not get a period of time between each character the user has typed (debounce time).

Most times that is an issue, since we might be doing requests to the server based on the provided input and you want to wait for the user to have finished typing; both for not doing many unnecessary requests but also to provide a better usability.

We can solve this problem quite simply through rxjs. Firstly, we need to create in our component a Subject of type string. This is what we are going to use to subscribe to changes on the inputted value and handle our debounce.

“Every Subject is an Observable and an Observer. You can subscribe to a Subject, and you can call next to feed values as well as error and complete.”

private _searchSubject: Subject<string> = new Subject();

Also, we are going to add to our component an Output that will emit an event with the inputted value after the debounce. Like that, it will be possible to make whichever request we need (or handle filter logic if the pagination is done in the frontend) bounded to that provided search string after the user has finished typing.

@Output() setValue: EventEmitter<string> = new EventEmitter();

Then, we need to create the subscription itself. We’ll do it using the pipe() function from rxjs.

“ You can use pipes to link operators together. Pipes let you combine multiple functions into a single function. The pipe() function takes as its arguments the functions you want to combine, and returns a new function that, when executed, runs the composed functions in sequence.”

constructor() {
this._setSearchSubscription();
}
private _setSearchSubscription() {
this._searchSubject.pipe(
debounceTime(500)
).subscribe((searchValue: string) => {
// Filter Function
});
}

We’ve also used debounceTime, an operator provided by the rxjs library; which receives a input of how many milliseconds it should wait before triggering the subscription. Here we are using 500ms, which I believe is a pretty decent period to wait “between key presses”.

Angular Docs has a nice simple page talking about the rxjs library, which you can check on the following link: https://angular.io/guide/rx-library
Explaining in-depth about RxJS is not the purpose of this article, but you can find out more in their documentation:
https://rxjs-dev.firebaseapp.com/api

Then, we need to create the method we are going to bind to the HTML input, which is going to trigger our Subject when the user types on the search bar (the html input element).

public updateSearch(searchTextValue: string) {
this._searchSubject.next( searchTextValue );
}

And let’s not forget to unsubscribe it on onDestroy to avoid memory leaks; as I’ve previously written in Understanding Angular Life Cycle Hooks.

ngOnDestroy() {
this._searchSubject.unsubscribe();
}

Finally, we need our template markup, which in this example is going to be quite simple; but of course in a real application one would customize and style it accordingly. In order to bind with the updateSearch method, we are going to use the keyup method.

<input
type="text"
(keyup)="updateSearch($event.target.value)"
/>

Final Code

So, putting all of our pieces together, this would be the final code for a search input component with a debounce strategy implemented. When used, this component provides an input that will trigger an event indicating the the user has finished typing so we can add any logic we need to it.

@Component({
selector: 'app-search-input',
template: `
<input
type="text"
[placeholder]="placeholder"
(keyup)="updateSearch($event.target.value)"
/>`
})
export class SearchInputComponent implements OnDestroy {
// Optionally, I have added a placeholder input for customization
@Input() readonly placeholder: string = '';
@Output() setValue: EventEmitter<string> = new EventEmitter();
private _searchSubject: Subject<string> = new Subject(); constructor() {
this._setSearchSubscription();
}
public updateSearch(searchTextValue: string) {
this._searchSubject.next( searchTextValue );
}
private _setSearchSubscription() {
this._searchSubject.pipe(
debounceTime(500)
).subscribe((searchValue: string) => {
this.setValue.emit( searchValue );
});
}
ngOnDestroy() {
this._searchSubject.unsubscribe();
}
}

Pagination Component

For our pagination component, what we have to do are two main things: render all the possible page numbers for the user to choose and detect when the page has changed in order to retrieve data from the chosen page.

First of all, we are going to add to our component an Input property to receive the informations we need to create the pagination: the pages size and the total amount of items.

export interface MyPagination {
itemsCount: number;
pageSize: number;
}
export class PaginationComponent { public pagesArray: Array<number> = [];
public currentPage: number = 1;
@Input() set setPagination(pagination: MyPagination) {
if (pagination) {
const pagesAmount = Math.ceil(
pagination.itemsCount / pagination.pageSize
);
this.pagesArray = new Array(pagesAmount).fill(1);
}
}
}

You will notice that instead of a direct input property I’ve used a setter to intercept the received input property. That was because of two things: to round the pagesAmount (in case that by any reason it was not already) and also to fill an Array of numbers with the pagesAmount.

Ok, so why do we need an Array of numbers? In order to render all the possible pages for the user. In Angular we can’t directly take a number and ask for a *ngFor loop an specific amount of times, so that is one strategy that I usually use to overcome this.

What we do is: using an array of numbers, we can loop through it and use the index to retrieve the number we want. Since what we want is just a regular ordered number list, it’s simple to achieve, as shown in the markup below.

<span
*ngFor="let page of pagesArray; let index = index"
[ngClass]="{ 'active': currentPage === index + 1 }
(click)="setPage(index + 1)"
>
{{ index + 1 }}
</span>

In this markup we are rendering all the possible pages for the user to choose. We have added an ngClass to set some style on the currently selected page in order to let the user know in which page he currently is. Also, we have inserted a click action that is going to emit an event letting the parent component know that the selected page has changed.

@Output() goToPage = new EventEmitter<number>();public setPage(pageNumber: number): void {  // Prevent changes if the same page was selected
if (pageNumber === this.currentPage)
return;
this.currentPage = pageNumber;
this.goToPage.emit(pageNumber);
}

Now, let’s also add two arrows to make our users life easier; one to go back one page and one to move forward one page. However, we are going to hide the left arrow when we are currently at the first page and hide the right arrow when we are currently at the last page.

<span
*ngIf="currentPage !== 1"
(click)="setPage(currentPage - 1)"
>
&lt; <!-- This is the simbol for '<' icon -->
</span>
<span
*ngFor="let page of pagesArray; let index = index"
[ngClass]="{ 'active': currentPage === index + 1 }
(click)="setPage(index + 1)"
>
{{ index + 1 }}
</span>
<span
*ngIf="currentPage !== pagesArray.length"
(click)="setPage(currentPage + 1)"
>
&gt; <!-- This is the simbol for '>' icon -->
</span>

But we still have one problem here! What if our itemsAmount are hundreds and our pageSize is small? Or even thousand of items? We would be rendering all of the pages at once and we would have a pretty bad usability with all those numbers just hanging there.

There are some possible design solutions for this problem, like hiding the middle pages or hiding the last pages after a certain number. The one I am going to show is simple to achieve and I believe can be interesting in some cases; which is changing the numbers from just being printed to use a select html element with each page as an option.

So, back to our markup, we are going to add the following changes to the part where we render our page numbers:

<!-- Here I decided the max pages amount before changing the rendering strategy to be 10, but you could change it however you want it and even create an environment variable if necessary -->
<ng-container *ngIf="pagesArray.length <= 10" >
<span
*ngFor="let page of pagesArray; let index = index"
[ngClass]="{ 'active': currentPage === index + 1 }"
(click)="setPage(index + 1)"
>
{{ index + 1 }}
</span>
</ng-container>
<ng-container *ngIf="pagesArray.length > 10" >
<select
[ngModel]="currentPage"
(ngModelChange)="setPage($event.target.value)"
>
<option
*ngFor="let p of pagesArray; let index = index"
[value]="(index + 1)" >
{{ index + 1 }}
</option>
</select>
</ng-container>

Final Code

So, putting all of our pieces together, this would be the final code for a simple but efficient pagination component implemented. It receives an input with the pages size and the total items amount and allows the user to select which page he wants to view, triggering an event indicating the selected page in order to handle the required pagination logic/requests.

export interface MyPagination {
itemsCount: number;
pageSize: number;
}
@Component({
selector: 'app-pagination',
template: `
<div class="pagination" >
<span
*ngIf="currentPage !== 1"
(click)="setPage(currentPage - 1)"
>
&lt;
</span>
<ng-container *ngIf="pagesArray.length <= 10" >
<span
*ngFor="let page of pagesArray; let index = index"
[ngClass]="{ 'active': currentPage === index + 1 }"
(click)="setPage(index + 1)"
>
{{ index + 1 }}
</span>
</ng-container>
<ng-container *ngIf="pagesArray.length > 10" >
<select
[ngModel]="currentPage"
(ngModelChange)="setPage($event.target.value)"
>
<option
*ngFor="let p of pagesArray; let index = index"
[value]="(index + 1)" >
{{ index + 1 }}
</option>
</select>
</ng-container>
<span
*ngIf="currentPage !== pagesArray.length"
(click)="setPage(currentPage + 1)"
>
&gt;
</span>`,
styleUrls: ['./pagination.component.scss']
})
export class PaginationComponent {
public pagesArray: Array<number> = [];
public currentPage: number = 1;
@Input() set setPagination(pagination: MyPagination) {
if (pagination) {
const pagesAmount = Math.ceil(
pagination.itemsCount / pagination.pageSize
);
this.pagesArray = new Array(pagesAmount).fill(1);
}
}
public setPage(pageNumber: number): void {
if (pageNumber === this.currentPage)
return;
this.currentPage = pageNumber;
this.goToPage.emit(pageNumber);
}
}

Example Usage

In order to provide a practical example, I’ll create an example component, where we have a paginated list of people and want to allow the user to search for a person’s name, select a page and filter the list results.

Search & Pagination Module

Firstly, we would create a Module for these components that we can import in the modules we are going to need it.

@NgModule({
declarations: [
SearchInputComponent,
PaginationComponent
],
imports: [
BrowserModule
],
exports: [
SearchInputComponent,
PaginationComponent
]
})
export class SearchAndPaginationModule { }

Then, importing the Module.

...@NgModule({
declarations: [
...
ListComponent
],
imports: [
...
SearchAndPaginationModule
],
providers: [
...
MyService
],
...
})
export class ExampleModule { }

Now, let’s assume we have a Service to communicate with a server to retrieve the users’ informations. Supposing this method receives a search string and the current page as parameters in order to filters the list results.

We are going to keep our markup really simple here, just in order to show how the components we have created could be used. Below, is how our final code would look like for a component that uses both our Search Component & Pagination Component combined.

@Component({
selector: 'app-list',
template: `
<app-search-input
placeholder="Search by name"
(setValue)="filterList($event)"
></app-search-input>
<ul>
<li *ngFor="let user of users" >
{{ user.name }}
</li>
</ul>
<app-pagination
[setPagination]="{
'itemsCount': totalUsersAmount,
'pageSize': 10
}"
(goToPage)="goToPage($event)"
></app-pagination>
`
})
export class ListComponent implements OnInit {
public users: Array<User>;
public totalUsersAmount: number = 0;
private _currentPage: number = 1;
private _currentSearchValue: string = '';
constructor(
private _myService: MyService
) { }
ngOnInit() {
this._loadUsers(
this._currentPage,
this._currentSearchValue
);
}
public filterList(searchParam: string): void {
this._currentSearchValue = searchParam;
this._loadUsers(
this._currentPage,
this._currentSearchValue
);
}
public goToPage(page: number): void {
this._currentPage = page;
this._loadUsers(
this._currentPage,
this._currentSearchValue
);
}
private _loadUsers(
page: number = 1, searchParam: string = ''
) {
this._myService.getUsers(
page, searchParam
).subscribe((response) => {
this.users = response.data.users;
this.totalUsersAmount = response.data.totalAmount;
}, (error) => console.error(error));
}
}

Hope it helps 😉

References

https://angular.io/guide/rx-library
https://itnext.io/understanding-angular-life-cycle-hooks-91616f8946e3
https://angular.io/guide/component-interaction#intercept-input-property-changes-with-a-setter

--

--