Using the Angular Material Paginator With ASP.NET Core and Angular
Using the Angular Material Paginator With ASP.NET Core and Angular
In this blog 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.
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 client side and only show a specific amount of entries to your users. What we do not want to do, is loading all items from the backend in the first place 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 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.
Frontend
The frontend is build with Angular and Angular Material. Watch 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 parameter which comes from the Angular Material Paginator. There every information about the current paging state is stored. We are passing this thing around to get the information about our state of paging having kind of an abstraction of the PageEvent of 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 consume its values to create the url sending the request to.
The Components
Beside the services, the components consume those services and the values. They are reacting on the pageswitch event and are separated in 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,
// ...
]
})
and then use it in our view like this:
ListComponent
The pageSize
and pageSizeOptions
come from the PaginationService
which we inject in the underlying component. On the (page)
event we are firing the eventemitter and call 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 shown before in this blog post.
Thanks for reading
Fabian
Links
https://angular.io/guide/http#reading-the-full-response
https://material.angular.io/components/paginator/overview
https://www.pluralsight.com/courses/asp-dot-net-core-restful-api-building