In the previous blog, I showed you how to connect Angular application with Spring back end, also with Spring Security and JWT. In this blog, I would like to show how to use Angular’s PrimeNG UI component library with Spring back end. Concretely, I will focus on the datatable lazy load filtering and pagination features.
As a demonstration domain, I’m going to use account transactions, which represent atomic “bank account” transactions, with properties like id, transaction date, amount, note and transaction category (in our case specified as an extra table names “transaction category”).
Technology stack
On the server side, I’m going to use Spring Boot, with in memory H2 database. For querying the database I’m using Querydsl, which is a JOOQ (Java Object Oriented Querying) API. Server-side operations are exposed using REST and for simplicity, no security is implemented. To build the server-side app, I’m using Apache Maven.
On the client side, I’m using Angular, version 4.2 with PrimeNG, version 4.1. Angular project was created using Angular CLI.
What we want to achieve
As I mentioned earlier, as a domain, we will use account transactions. We want to display account transactions in a paginated table with filtering options for amount (equals filter) and category (with multiple checkbox option). It is possible to implement this in 2 ways:
I will demonstrate option #2, as it is more often used when displaying larger datasets and from the implementation point of view, it is also more complex to implement.
Server side
Spring boot application, that represents server in our case, is available in https://remenec_jakub@bitbucket.org/remenec_jakub/primeng-lazy-showcase.git repository and it is called budget-app-server. I’m not going to describe in detail how Maven and Spring works, but I’m going to focus on the pagination and filtering itself. Server-side app is divided into several packages:
When a request is called from the Angular client, the first class contacted on the server side is AccountTransactionEndpoint.
As you can see, it has only one operation called getAccountTransactionsPaginated. Complete path to this operation is http://{serverPath}/transactions/list. I decided to use POST, in order to handle more complex filtering options in the future. This method is also annotated with CrossOrigin annotation:
@CrossOrigin(origins = "http://localhost:4200")
@RequestMapping( value = "/list", method = RequestMethod.POST)
@ResponseBody
public AccountTransactionsResponse getAccountTransactionsPaginated(@RequestBody AccountTransactionRequest accountTransactionRequest){
Logger.getAnonymousLogger().log(Level.INFO, "List accountTransactions operation called");
return createResponse(accountTransactionFacade.findAccountTransactions(accountTransactionRequest));
}
It is better to configure cross origin, but to save time and make this showcase work in an easier way, I add origin manually hardcoded for this endpoint. The getAccountTransactionsPaginated request contains:
The operation only logs that it was called and calls AccountTransactionFacade.findAccountTransactions method and passes the request in.
In AccountTransactionFacade, I’m using Querydsl to prepare filter for Spring repository. First of all, I create BooleanExpression filters that are based on incoming request using extractFilters method as follows:
List filters = new ArrayList<>();
QAccountTransaction accountTransaction = QAccountTransaction.accountTransaction;
if (accountTransactionRequest.getAmount() != null) {
filters.add(accountTransaction.amount.eq(accountTransactionRequest.getAmount()));
}
if (accountTransactionRequest.getCategoriesToFilter() != null && !accountTransactionRequest.getCategoriesToFilter().isEmpty()) {
filters.add(accountTransaction.transactionCategory.categoryName.in(accountTransactionRequest.getCategoriesToFilter()));
}
return filters;
You can see that, I’m initializing QAccountTransaction using QAccountTransaction.accountTransaction. How was the Q-prefixed object created? It is quite simple. The only things that have to be done are:
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>maven-apt-plugin</artifactId>
<version>1.0</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources</outputDirectory>
<processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
This way the Q-prefixed classes will be generated for all your JPA entities to target folder using JPAAnnotationProcessor.
When we get back to the filtering code, you can see that there are 2 filters added. One of them is for amount equal to amount from request:
accountTransaction.amount.eq(accountTransactionRequest.getAmount())
and the second one for transactionCategory.categoryName in the string list of categories to filter provided in the request:
accountTransaction.transactionCategory.categoryName.in(accountTransactionRequest.getCategoriesToFilter())
When all filters are ready in the list, we can chain them using and operator (as in SQL). I’m doing this in chainFiltersWithAnd method as follows:
BooleanExpression result = null;
for (BooleanExpression booleanExpression : filters) {
if (result == null) {
result = booleanExpression;
} else {
result = result.and(booleanExpression);
}
}
return result;
This method returns only one BooleanExpression build from list of boolean expressions. When BooleanExpression is ready, we can call the Spring repository for account transactions as follows:
return accountTransactionRepository.findAll(
chainFiltersWithAnd(filters),
new PageRequest(accountTransactionRequest.getPage(), accountTransactionRequest.getSize())
);
First parameter is our BooleanExpression and the second is PageRequest object, which contains page, page size and optionally the sorting information.
There is one more thing required for such call to repository. The repository has to be a JpaRepository and has to implement also the QueryDslPredicateExecutor.
public interface AccountTransactionRepository extends JpaRepository<AccountTransaction, Integer>, QueryDslPredicateExecutor
If there are no filters, we don’t need to call the repository method with BooleanExpression, but we can call it only with the PageRequest as follows:
return accountTransactionRepository.findAll(
new PageRequest(accountTransactionRequest.getPage(), accountTransactionRequest.getSize())
);
Client side
Angular client application that represents our client is available in https://remenec_jakub@bitbucket.org/remenec_jakub/primeng-lazy-showcase.git repository in transaction-app folder. It was created using Angular CLI (https://cli.angular.io/). It is a standard Angular application. For UI part, I used PrimeNG component library (https://www.primefaces.org/primeng/). All you need to do for this is to add PrimeNG dependency to package.json for version 4.1+:
"primeng": "^4.1.0"
and to specify style paths for css for themes in _angular-cli.json:
"styles": [
"../node_modules/primeng/resources/primeng.css",
"../node_modules/primeng/resources/themes/omega/theme.scss",
"../node_modules/font-awesome/css/font-awesome.css",
"styles.css"
],
That’s it! No more configuration is necessary.
Now, it is needed to specify domain/communication model on the client side. This is done in src/model folder. There you can find account-transaction.ts, account-transaction-request.ts and transaction-category.ts. These are practically one to one copies with objects you can find in server application:
transaction category:
export class TransactionCategory { constructor(public categoryName: string, public categoryDescription: string) { } }
account transaction:
export class AccountTransaction { constructor(public transactionDate: Date, public amount: number, public note: string, public transactionCategory: TransactionCategory) { } }
account transaction request:
export class AccountTransactionRequest {
constructor(public page: number,
public size: number,
public categoriesToFilter: string[],
public amount: number) {
}
}
When the model is ready, we can start working on the table. Let’s go to src/app folder. First of all, I’m defining AppModule module configuration:
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
FormsModule,
DataTableModule,
HttpModule,
InputTextModule,
SharedModule,
MultiSelectModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
In this module, there are all necessary imports for implementation of lazy loaded datatable.
When AppModule is finished, we can move to AppComponent. This class has 3 main parameters:
transactions: AccountTransaction[];
totalRecords: number;
categories: SelectItem[];
Transactions are the AccountTransaction objects retrieved from the server. TotalRecords is a number of all account transactions that correspond to the filter. The attribute “Categories” is a array of select items which are used in category MultiSelect input filter. The categories variable is initialized in ngOnlnit method as follows:
this.categories.push({label: 'Clothes', value: 'Clothes'});
this.categories.push({label: 'Food', value: 'Food'});
this.categories.push({label: 'Regular expenses', value: 'Regular expenses'});
this.categories.push({label: 'Income', value: 'Income'});
this.categories.push({label: 'Other', value: 'Other'});
Label represents the value that will be visible to user in select element and values is the value behind the item. The “value” is sent to the server and the filtering from the database is done based on it.
The last part of the components is the loadTransactionsLazy method. This method is created to handle the call from the datatable to perform lazy loading. This function is called each time the used performs a change to any filter or clicks to display another page in the datatable.
As you can see, it accepts LazyLoadEvent object which has the following structure:
In filters node, there is a named array with all the filters available in the table. Inside each of these filters there is an information about matchMode (equals – exact match, in – value is in an array, etc.) and value, which is the concrete value/values to be filtered.
Furthermore, there is information about sorting and paging. Field first describes the index of the first element on the page. When page size is 5 elements, then the first can be {0,6,11,…}. Rows field tells us how big is the page (in other words, how many elements are there per page).
Let’s go back to the component code. On the first line, I extract transactionCategory.categoryName filter from the event. Inside its ‘value’, there is an array of category names to filter (strings). This value can be directly used in our request.
On the 3rd line, I get the amount filter value, which is also directly ready to be used in the request as a number.
Before calling the operation, we need to create the AccountTransactionRequest. It needs the:
When all the necessary data is prepared, we can call the POST to REST endpoint on the server, which handles filtering. It is the /transactions/list operation. After a successful response, I fill the transactions field and the totalRecords field (how many records are there in the database matching the current filter options – this is necessary for the component to determine how many pages are there for the paginator to display).
Now, let’s check the html file for the app component.
<p-dataTable [value]="transactions" [rows]="5" [paginator]="true" [lazy]="true" [totalRecords]="totalRecords" (onLazyLoad)="loadTransactionsLazy($event)" #dt>
<p-header>List of Cars</p-header>
<p-column field="transactionDate" header="Date"></p-column>
<p-column field="amount" header="Amount" [filter]="true" filterMatchMode="equals"></p-column>
<p-column field="transactionCategory.categoryName" header="Category" [filter]="true" filterMatchMode="in">
<ng-template pTemplate="filter" let-col>
<p-multiSelect [options]="categories" defaultLabel="All categories" (onChange)="dt.filter($event.value,col.field,col.filterMatchMode)"
styleClass="ui-column-filter"></p-multiSelect>
</ng-template>
</p-column>
</p-dataTable>
The datatable displays transactions from transactions attribute in component. As you can see, I set paginator = true, to use the paginator. By setting rows = 5, I tell the component to display 5 items per page. The lazy load magic comes with the lazy attribute and onLazyLoad callback. The onLazyLoad callback calls the loadTransactionsLazy method, and passes in the $event where all the necessary data is available to perform lazy loading. We have 2 filters: the amount filter and categories filter.
The amount filter is set up in an easier way. It is sufficient to set filter = true and fill also filterMatchMode to equals. There are several other filterMatchModes which you can find in primeng docs.
The categories filter is more complex. As you can see it has also filter = true, but filterMatchMode = in. Then I use multiselect element to choose available filtering options. This one is wrapped in an ng-template component. The multiselect takes options from categories attribute, defined in the component. However, the multiselect component calls dt.onChange – the filter function on datatable (dt – see that #dt is defined within p-dataTable element).
Result
Now, you can launch the server (java -jar {path to jar file} and client (using ‘ng serve’) and after opening http://localhost:4200/ you will see the following output:
Good luck! In case you need a hand or have any questions, let me know!
Do you see yourself working with us? Check out our vacancies. Is your ideal vacancy not in the list? Please send an open application. We are interested in new talents, both young and experienced.
Join us