Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Using the Angular Material Paginator With ASP.NET Core and Angular

DZone's Guide to

Using the Angular Material Paginator With ASP.NET Core and Angular

In this post, I want to show you how to use Angular Material with Angular to use a table with paging which is driven by an ASP.NET Core WebAPI.

· Web Dev Zone ·
Free Resource

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

Code

You can find the code here: https://github.com/FabianGosebrink/ASPNETCore-Angular-Material-HATEOAS-Paging

Get Started

With the Angular Material Table and its Pagination Module, it is quite easy to set up paging in a beautiful way so that you can use it on the client-side and only show a specific amount of entries to your users. What we do not want to do is load all the items from the backend in the first place just to get the paging going and then display only a specific amount. Instead, we want to load only what we need and display that. If the user clicks on the “next page” button, the items should be loaded and displayed.

The Backend

The backend is an ASP.NET Core WebAPI which sends out the data as JSON. With it, every entry contains the specific links and also all links containing the paging links to the next page, previous page, etc., although we do not need them in this example because we already have some implemented logic from Angular Material. If you would not use Angular Material or another “intelligent” UI piece giving you a paging logic, you could use the links to make it all by yourself.

Customer Controller

[Route("api/[controller]")]
public class CustomersController : Controller
{
	[HttpGet(Name = nameof(GetAll))]
	public IActionResult GetAll([FromQuery] QueryParameters queryParameters)
	{
		List<Customer> allCustomers = _customerRepository
			.GetAll(queryParameters)
			.ToList();

		var allItemCount = _customerRepository.Count();

		var paginationMetadata = new
		{
			totalCount = allItemCount,
			pageSize = queryParameters.PageCount,
			currentPage = queryParameters.Page,
			totalPages = queryParameters.GetTotalPages(allItemCount)
		};

		Response.Headers
			.Add("X-Pagination", 
				JsonConvert.SerializeObject(paginationMetadata));

		var links = CreateLinksForCollection(queryParameters, allItemCount);

		var toReturn = allCustomers.Select(x => ExpandSingleItem(x));

		return Ok(new
		{
			value = toReturn,
			links = links
		});
	}
}

We are sending back the information about the paging with HATEOAS but also with a header to read it with Angular later. The totalcount is especially interesting for the client. You could also send this back with the JSON response.

var paginationMetadata = new
{
    totalCount = allItemCount,
    // ...
};

Response.Headers
    .Add("X-Pagination", 
        JsonConvert.SerializeObject(paginationMetadata));

If you do send it back via the header, be sure to expand the headers in CORS that they can be read on the client-side.

services.AddCors(options =>
{
    options.AddPolicy("AllowAllOrigins",
        builder => builder.AllowAnyOrigin()
        .AllowAnyMethod()
        .AllowAnyHeader()
        .AllowCredentials()
        .WithExposedHeaders("X-Pagination"));
});

There is also a parameter which can be passed to the GetAll method: QueryParameters.

public class QueryParameters
{
    private const int maxPageCount = 50;
    public int Page { get; set; } = 1;

    private int _pageCount = maxPageCount;
    public int PageCount
    {
        get { return _pageCount; }
        set { _pageCount = (value > maxPageCount) ? maxPageCount : value; }
    }

    public string Query { get; set; }

    public string OrderBy { get; set; } = "Name";
}

The modelbinder from ASP.NET Core can map the parameters in the request to this object and you can start using them as follows: http://localhost:5000/api/customers?pagecount=10&page=1&orderby=Name is a valid request then which gives us the possibility to grab only the range of items we want to.

Front-End

The front-end is built with Angular and Angular Material. We'll get into the details below.

PaginationService

This service is used to collect all the information about the pagination. We are injecting the PaginationService and consuming its values to create the URL and send the request.

@Injectable()
export class PaginationService {
    private paginationModel: PaginationModel;

    get page(): number {
        return this.paginationModel.pageIndex;
    }

    get selectItemsPerPage(): number[] {
        return this.paginationModel.selectItemsPerPage;
    }

    get pageCount(): number {
        return this.paginationModel.pageSize;
    }

    constructor() {
        this.paginationModel = new PaginationModel();
    }

    change(pageEvent: PageEvent) {
        this.paginationModel.pageIndex = pageEvent.pageIndex + 1;
        this.paginationModel.pageSize = pageEvent.pageSize;
        this.paginationModel.allItemsLength = pageEvent.length;
    }
}

We are exposing three properties here which can be changed through the “change()” method. The method takes a pageEvent as a parameter which comes from the Angular Material Paginator. There, all the information about the current paging state is stored. We are passing this thing around to get the information about our state of paging, which has kind of an abstraction of the pageEvent from Angular Material.

HttpBaseService

@Injectable()
export class HttpBaseService {

    private headers = new HttpHeaders();
    private endpoint = `http://localhost:5000/api/customers/`;

    constructor(
        private httpClient: HttpClient,
        private paginationService: PaginationService) {

        this.headers = this.headers.set('Content-Type', 'application/json');
        this.headers = this.headers.set('Accept', 'application/json');
    }

    getAll<T>() {
        const mergedUrl = `${this.endpoint}` +
            `?page=${this.paginationService.page}&pageCount=${this.paginationService.pageCount}`;

        return this.httpClient.get<T>(mergedUrl, { observe: 'response' });
    }

    getSingle<T>(id: number) {
        return this.httpClient.get<T>(`${this.endpoint}${id}`);
    }

    add<T>(toAdd: T) {
        return this.httpClient.post<T>(this.endpoint, toAdd, { headers: this.headers });
    }

    update<T>(url: string, toUpdate: T) {
        return this.httpClient.put<T>(url,
            toUpdate,
            { headers: this.headers });
    }

    delete(url: string) {
        return this.httpClient.delete(url);
    }
}

We are injecting the PaginationService and consuming its values to create the URL to which we are sending the request.

The Components

Beside the services, the components consume those services and the values. They are reacting on the pageswitch event and are separated into stateful and stateless components.

Include in Module

In the ListComponent we are now using the paginator module, but, first, we have to include it in our module like this:

import { MatPaginatorModule } from '@angular/material/paginator';

@NgModule({
    imports: [
        MatPaginatorModule,
        // ...
    ]
})

Now we can use it in our view like this:

ListComponent

<div class="example-container mat-elevation-z8">
    <mat-table #table [dataSource]="dataSource" matSort>

      <ng-container matColumnDef="id">
        <mat-header-cell *matHeaderCellDef mat-sort-header> No. </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{element.id}} </mat-cell>
      </ng-container>

      <ng-container matColumnDef="name">
        <mat-header-cell *matHeaderCellDef mat-sort-header> Name </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
      </ng-container>

      <ng-container matColumnDef="created">
        <mat-header-cell *matHeaderCellDef mat-sort-header> Created </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{element.created | date}} </mat-cell>
      </ng-container>

      <ng-container matColumnDef="actions">
        <mat-header-cell *matHeaderCellDef mat-sort-header> Actions </mat-header-cell>
        <mat-cell *matCellDef="let element"> 
          <button mat-icon-button (click)="onDeleteCustomer.emit(element)"><mat-icon>delete</mat-icon></button> 
          <a mat-icon-button [routerLink]="['/details', element.id]"><mat-icon>edit</mat-icon></a>
        </mat-cell>
      </ng-container>

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


  <mat-paginator [length]="totalCount"
    [pageSize]="paginationService.pageSize"
    [pageSizeOptions]="paginationService.selectItemsPerPage" 
    (page)="onPageSwitch.emit($event)">
  </mat-paginator>

The pageSize and pageSizeOptions come from the PaginationService which we injected in the underlying component. On the (page) event, we are firing the eventemitter and calling the action which is bound to it in the stateful component.

export class ListComponent {

    dataSource = new MatTableDataSource<Customer>();
    displayedColumns = ['id', 'name', 'created', 'actions'];

    @Input('dataSource')
    set allowDay(value: Customer[]) {
        this.dataSource = new MatTableDataSource<Customer>(value);
    }

    @Input() totalCount: number;
    @Output() onDeleteCustomer = new EventEmitter();
    @Output() onPageSwitch = new EventEmitter();

    constructor(public paginationService: PaginationService) { }
}

As the ListComponent is a stateless service, it gets passed all the values it needs when using it on the stateful component, OverviewComponent

OverviewComponent

<app-list 
    [dataSource]="dataSource" 
    [totalCount]="totalCount"
    (onDeleteCustomer)="delete($event)"
    (onPageSwitch)="switchPage($event)"
    ></app-list>
export class OverviewComponent implements OnInit {

    dataSource: Customer[];
    totalCount: number;

    constructor(
        private customerDataService: CustomerDataService,
        private paginationService: PaginationService) { }

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

    switchPage(event: PageEvent) {
        this.paginationService.change(event);
        this.getAllCustomers();
    }

    delete(customer: Customer) {
        this.customerDataService.fireRequest(customer, 'DELETE')
            .subscribe(() => {
                this.dataSource = this.dataSource.filter(x => x.id !== customer.id);
            });
    }

    getAllCustomers() {
        this.customerDataService.getAll<Customer[]>()
            .subscribe((result: any) => {
                this.totalCount = JSON.parse(result.headers.get('X-Pagination')).totalCount;
                this.dataSource = result.body.value;
            });
    }
}

The switchPage method is called when the page changes and first sets all the new values in the paginationService and then gets the customers again. Those values are then provided again in the DataService, and are consumed there, and also used in the view where they get displayed correctly.

In the getAllCustomers method, we are reading the totalCount value from the headers. Be sure to read the full response in the DataService by adding return this.httpClient.get<T>(mergedUrl, { observe: 'response' }); and exposing the header in the CORS options like we did earlier. 

Thanks for reading!

Fabian

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

Topics:
web dev ,angular ,asp.net core ,pagination ,angular material ,full-stack development

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}