<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kirill Kovzel</title>
    <description>The latest articles on DEV Community by Kirill Kovzel (@pseudopilot).</description>
    <link>https://dev.to/pseudopilot</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1406029%2F7122084d-6a55-425a-83c4-a3190b888ca9.png</url>
      <title>DEV Community: Kirill Kovzel</title>
      <link>https://dev.to/pseudopilot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pseudopilot"/>
    <language>en</language>
    <item>
      <title>Building custom pagination component with Angular</title>
      <dc:creator>Kirill Kovzel</dc:creator>
      <pubDate>Wed, 08 May 2024 22:34:46 +0000</pubDate>
      <link>https://dev.to/pseudopilot/creating-custom-pagination-component-with-angular-2dl2</link>
      <guid>https://dev.to/pseudopilot/creating-custom-pagination-component-with-angular-2dl2</guid>
      <description>&lt;p&gt;Pagination is undoubtedly one of the most popular approaches to dozed content representation. And it’s absolutely fine to use some out-of-the-box solutions for it if your application uses some library like &lt;a href="https://ng-bootstrap.github.io/#/components/pagination/overview"&gt;NgBootstrap&lt;/a&gt; or &lt;a href="https://material.angular.io/components/paginator/overview"&gt;Material UI&lt;/a&gt; all round and the designs and functionality they provide is satisfactory to you.&lt;br&gt;
But once you bump into the need of re-styling or customisation of it you usually may choose the way of creating a wrapping component which uses this out-of-the-box component inside and re-define its behaviour or styles by breaking original component encapsulation.&lt;br&gt;
This is a legit approach of course if there are few changes to make. But when you start modifying it over and over trying to satisfy some special customer needs there may be a point when you say: “Come on! It would be easier and faster to create it from scratch than to do all these customisations!”&lt;br&gt;
And you will be absolutely right here! Let’s explore together the way how to implement it in your Angular app.&lt;/p&gt;

&lt;p&gt;Impatient readers can find the whole project &lt;a href="https://stackblitz.com/edit/angular-ivy-t8kaef"&gt;here&lt;/a&gt;. The rest are welcome to follow with me step by step.&lt;/p&gt;



&lt;p&gt;I like to think about pagination as about select control: we have the list of options (pages) and select one of them. And this is the reason to implement it as a form control. And speaking in terms of Angular it means that we want to implement &lt;code&gt;ControlValueAccessor&lt;/code&gt; interface. There is an official documentation and a lot of other instructions and descriptions how to implement it. So, we’re not going to dwell on it and only will deal with pagination specific of it.&lt;/p&gt;

&lt;p&gt;Of course you may say that we could make this component without implementing ControlValueAccessor interface just using &lt;code&gt;@Output&lt;/code&gt; property for handling page change events. This point of view is absolutely reasonable and furthermore we’ll have this version of our component here at first as well. But the benefit of implementing ControlValueAccessor is that it gives us more flexibility: we can use or pagination component either as a template form with one- or two-way data binding &lt;code&gt;([(ngModel)]&lt;/code&gt; or &lt;code&gt;[ngModel]&lt;/code&gt;+ &lt;code&gt;(ngModelChange)&lt;/code&gt;) or as a reactive form control (&lt;code&gt;[formControl]&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The reactive approach can be very handy when our pagination is supposed to be used in combination with other form controls. For example, we may have a table component which offers user a possibility to sort and filter data by columns, use pagination and table lookup. In this case we can use &lt;strong&gt;RxJS&lt;/strong&gt; &lt;code&gt;combineLatest&lt;/code&gt; method to subscribe and listen to the changes of all these controls only once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;combineLates([
  this.searchControl.pipe(
    startWith(this.searchControl.value),
    debounceTime(500)
  ),
  this.filterControl.pipe(startWith(this.filterControl.value)),  
  this.sortControl.pipe(startWith(this.sortControl.value)),
  this.pageControl.pipe(
    startWith(this.pageControl.value),
    debounceTime(500)
  )
]).subscribe((search, filter, sort, pagination) =&amp;gt; {
  this.sendRequest({
    search,
    filter,
    sortBy: sort.sortBy,
    sortDirection: sort.sortDirection,
    page: pagination.page,
    pageSize: pagination.pageSize
  };
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we you can see above with this approach we have to pipe &lt;code&gt;startWith&lt;/code&gt; to each control, otherwise our subscription won’t start until each control emits at leas one value. In this example we use the initial form values to start with.&lt;/p&gt;

&lt;p&gt;Another advantage of reactive approach is the possibility to use &lt;code&gt;debounceTime&lt;/code&gt; to particular controls which allows us to avoid triggering requests on each key stroke in search input or on each click on pagination &lt;strong&gt;Previous&lt;/strong&gt; / &lt;strong&gt;Next&lt;/strong&gt; button.&lt;/p&gt;




&lt;p&gt;So, let’s finally start with pagination itself!&lt;/p&gt;

&lt;p&gt;At the first step for the visualising purpose we create a simple array of 100 elements which we’ll paginate later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// app.component.html

&amp;lt;div class="items-wrapper"&amp;gt;
  &amp;lt;my-card
    *ngFor="let item of visibleItems.items"
    [item]="item"&amp;gt;
  &amp;lt;/my-card&amp;gt;
&amp;lt;/div&amp;gt;


// app.component.ts

export interface PaginatedResponse&amp;lt;T&amp;gt; {
  items: T[];
  total: number;
}

...

public pageSize = 10;

private readonly items = Array.from(
  Array(100).keys(),
  (item) =&amp;gt; item + 1
);

public visibleItems: PaginatedResponse&amp;lt;number&amp;gt; = {
  items: this.items.slice(0, this.pageSize),
  total: this.items.length,
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a rule the backend sends us a paginated response, which have at least 2 properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;items&lt;/code&gt; — array of items for the particular (requested) page&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;total&lt;/code&gt; — how many matching items they have in the whole.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’re going to follow this structure in our example even though we don’t have any backend in this example. We’ll just be slicing needed range of the initial array imitating backend responses.&lt;/p&gt;




&lt;p&gt;On the second step we create a simple pagination component which would allow us to navigate to the first, previous, next, last and particular page from the visible range with no interactions with parent component.&lt;/p&gt;

&lt;p&gt;I’m saying &lt;em&gt;visible range&lt;/em&gt; because we may have our content split into dozens or hundreds of pages, and we definitely don’t want to show all of them to the user. So we provide &lt;code&gt;visibleRangeLength&lt;/code&gt; property to limit the visible pages count.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// pagination.component.html

&amp;lt;div class="pagination"&amp;gt;
  &amp;lt;button
    [disabled]="value === 1"
    (click)="selectPage(1)"&amp;gt;
    &amp;lt;&amp;lt;
  &amp;lt;/button&amp;gt;
  &amp;lt;button
    [disabled]="value === 1"
    (click)="selectPage(value - 1)"&amp;gt;
    &amp;lt;
  &amp;lt;/button&amp;gt;
  &amp;lt;button
    *ngFor="let page of visiblePages"
    [ngClass]="value === page &amp;amp;&amp;amp; 'selected'"
    (click)="selectPage(page)"&amp;gt;
    {{ page }}
  &amp;lt;/button&amp;gt;
  &amp;lt;button
    [disabled]="value === totalPages
    (click)="selectPage(value + 1)"&amp;gt;
    &amp;gt;
  &amp;lt;/button&amp;gt;
  &amp;lt;button
    [disabled]="value === totalPages"
    (click)="selectPage(totalPages)&amp;gt;
    &amp;gt;&amp;gt;
  &amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;


// pagination.component.ts

public value = 1;
public totalPages = 10;
public visibleRangeLength = 5;
public visiblePages: number[];

ngOnInit(): void {
  this.updateVisiblePages();
}

public selectPage(page: number): void {
  this.value = page;
  this.updateVisiblePages();
}

private updateVisiblePages(): void {
  const length = Math.min(this.totalPages, this.visibleRangeLength);
  const startIndex = Math.max(
    Math.min(
      this.value - Math.ceil(length / 2),
      this.totalPages - length
    ),
    0
  );
  this.visiblePages = Array.from(
    new Array(length).keys(),
    (item) =&amp;gt; item + startIndex + 1
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only tricky part here I guess is &lt;code&gt;updateVisiblePages&lt;/code&gt; method. The main points of it are the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;we always want to have visible as many pages as we set in &lt;code&gt;visibleRangeLength&lt;/code&gt; property&lt;/li&gt;
&lt;li&gt;if &lt;code&gt;visibleRangeLength&lt;/code&gt; turns out to be bigger than &lt;code&gt;totalPages&lt;/code&gt; count (and this is absolutely possible as we never know how many pages the backend has for us, especially if we request filtered results) we want to display &lt;code&gt;totalPages&lt;/code&gt; count&lt;/li&gt;
&lt;li&gt;the selected page should be in the middle of the visible range if possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now this component is already functional but it can’t interact with external environment. To turn our component into useable configurable solution we need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;convert &lt;code&gt;value&lt;/code&gt;, &lt;code&gt;totalPages&lt;/code&gt;, &lt;code&gt;visibleRangeLength&lt;/code&gt; into component inputs&lt;/li&gt;
&lt;li&gt;to add component output &lt;code&gt;valueChange&lt;/code&gt; and extend &lt;code&gt;selectPage&lt;/code&gt; method with the line &lt;code&gt;this.valueChange.emit(this.value)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Though we can refactor it a little bit. As we have said previously we usually don't get &lt;code&gt;totalPages&lt;/code&gt; value from the backend, but we do get total items quantity and then we calculate the &lt;code&gt;totalPages&lt;/code&gt; on our side depending on the page size. So, instead of having this logic in the &lt;strong&gt;AppComponent&lt;/strong&gt; let’s put it in the &lt;strong&gt;PaginationComponent&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;convert &lt;code&gt;totalPages&lt;/code&gt; back into the regular public property&lt;/li&gt;
&lt;li&gt;provide input properties &lt;code&gt;total&lt;/code&gt; (for the total items count) and &lt;code&gt;pageSize&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;add re-calculation of the &lt;code&gt;totalPages&lt;/code&gt; value each time our &lt;code&gt;total&lt;/code&gt; or &lt;code&gt;pageSize&lt;/code&gt; change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After finishing it our component looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// pagination.component.ts

@Input() value = 1;
@Input() total = 10;
@Input() pageSize = 10;
@Input() visibleRangeLength = 5;
@Output() valueChange = new EventEmitter&amp;lt;number&amp;gt;();

public totalPages: number;
public visiblePages: number[];

ngOnInit(): void {
  this.updateVisiblePages();
}

ngOnChanges(changes: SimpleChanges): void {
  if (changes.total || changes.pageSize) this.updateTotalPages();
}

public selectPage(page: number): void {
  this.value = page;
  this.updateVisiblePages();
  this.valueChange.emit(this.value);
}

private updateVisiblePages(): void {
  const length = Math.min(this.totalPages, this.visibleRangeLength);
  const startIndex = Math.max(
    Math.min(
      this.value - Math.ceil(length / 2),
      this.totalPages - length
    ),
    0
  );
  this.visiblePages = Array.from(
    new Array(length).keys(),
    (item) =&amp;gt; item + startIndex + 1
  );
}

private updateTotalPages(): void {
  this.totalPages = Math.ceil(this.total / this.pageSize);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can apply the pagination to our list of items:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// app.component.html

...

&amp;lt;div class="pagination-wrapper"&amp;gt;
  &amp;lt;my-pagination
    [total]="visibleItems.total"
    [pageSize]="pageSize"
    (valueChange)="onPageChange($event)"&amp;gt;
  &amp;lt;/my-pagination&amp;gt;
&amp;lt;/div&amp;gt;


// app.component.ts

...

public onPageChange(page: number): void {
  const startIndex = (page - 1) * this.pageSize;
  const items = this.items.slice(
    startIndex,
    startIndex + this.pageSize
  );
  this.visibleItems = { items, total: this.items.length };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;On the third step I want to make our component responsible not only for the page but also for the page size selection. So we’ll add the select element to our pagination template and extend and refactor the component logic a bit more.&lt;/p&gt;

&lt;p&gt;Obviously, our component should emit page size change event outside. But the business logic may be different here depending on the customer preferences. One may want the page content not to be updated on page size change and only request new data set on page number change. Another may have different thoughts about it.&lt;/p&gt;

&lt;p&gt;I personally think that the most transparent way to handle it would be to request the new range of data on the page size change reseting the selected page to 1 in the same time. And this is the approach we’re going to implement further.&lt;/p&gt;

&lt;p&gt;At first we’ll extend &lt;code&gt;value&lt;/code&gt; interface so that it includes two properties &lt;code&gt;page&lt;/code&gt; and &lt;code&gt;pageSize&lt;/code&gt;. This will also need to do some refactoring in the existing &lt;strong&gt;PaginationComponent&lt;/strong&gt; code (like replacing &lt;code&gt;value&lt;/code&gt; with &lt;code&gt;value.page&lt;/code&gt; etc.).&lt;/p&gt;

&lt;p&gt;Then we’ll add the following changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// pagination.component.html

...

&amp;lt;select
  [ngModel]="value.pageSize"
  (ngModelChange)="selectPageSize($event)"&amp;gt;
  &amp;lt;option
    *ngFor="let size of pageSizes"
    [value]="size"&amp;gt;
    {{ size }}
  &amp;lt;/option&amp;gt;
&amp;lt;/select&amp;gt;


// pagination.component.ts

...

export interface PaginationValue {
  page: number;
  pageSize: number;
}
...
@Input() value: PaginationValue = { page: 1, pageSize: 5 };
...
@Input() pageSizes: number[] = [5, 10, 25, 50];
@Output() valueChange = new EventEmitter&amp;lt;PaginationValue&amp;gt;();
...
ngOnChanges(changes: SimpleChanges): void {
  if (changes.total || changes.value) this.updateTotalPages();
}

public selectPage(page: number): void {
  this.value = { ...this.value, page };
  ...
}

public selectPageSize(pageSize: string): void {
  this.value = { page: 1, pageSize: +pageSize };
  this.updateTotalPages();
  this.updateVisiblePages();
  this.valueChange.emit(this.value);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;And finally let’s turn our component into the &lt;code&gt;ControlValueAccessor&lt;/code&gt;. For this we only need to remove everything related to the output property &lt;code&gt;valueChange&lt;/code&gt; and add the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component({
  ...
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() =&amp;gt; PaginationComponent),
    multi: true,
  }]
})

export class PaginationComponent implements OnInit, OnChanges, ControlValueAccessor {
  ...
  onChange(value: any) {};
  onTouched() {};

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  writeValue(value: PaginationValue): void {
    if (!value) return;
    this.value = value;
    this.updateTotalPages();
    this.updateVisiblePages();
  }

  public selectPage(page: number): void {
    ...
    this.onChange(this.value);
  }

  public selectPageSize(pageSize: number): void {
    ...
    this.onChange(this.value);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now we can use the pagination as a form control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// app.component.html

...

&amp;lt;my-pagination
  [total]="visibleItems.total"
  [formControl]="paginationControl"&amp;gt;
&amp;lt;/my-pagination&amp;gt;

&amp;lt;!-- OR  --&amp;gt;

&amp;lt;my-pagination
  [total]="visibleItems.total"
  [ngModel]="pagination"
  (ngModelChange)="onPageChange($event)"&amp;gt;
&amp;lt;/my-pagination&amp;gt;


// app.component.ts

...

public pagination = { page: 1, pageSize: 10 };

// OR

public readonly paginationControl = new FormControl(this.pagination);

ngOnInit(): void {
  this.paginationControl.valueChanges
    .subscribe(this.onPageChange.bind(this));
}

public onPageChange(pagination: PaginationValue): void {
  const startIndex = (pagination.page - 1) * pagination.pageSize;
  const items = this.items.slice(
    startIndex,
    startIndex + pagination.pageSize
  );
  this.visibleItems = { items, total: this.items.length };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thank you for reading it and for your feedbacks!&lt;/p&gt;

</description>
      <category>angular</category>
      <category>pagination</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
