1. Introduction
The NgZorro
is a set of Angular UI components we can use in complex Angular applications. This article will show you how to use Angular NgZorro Table Component with Pagination, Filtering, and Sorting functionalities.
More articles related to NgZorro
:
2. Overview
The application will use Angular as a frontend framework, Spring Boot on the back-end site, and PostgreSQL database to persist any submitted data. Angular Table Component is an element very often used in applications to present data structurally.
3. Spring boot application
Let's start with the back-end site of the app. Spring Boot will be our main server responsible for communicating with the front-end site using REST API.
3.1. Maven dependencies
First, to create back-end site we need to make a Maven project, with the following pom.xml
file:
Copy
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-boot-postgresql-angular-ng-zorro-table</artifactId>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
<relativePath/>
</parent>
<properties>
<lombok.version>1.18.24</lombok.version>
<liquibase-core.version>4.10.0</liquibase-core.version>
</properties>
<!-- Add typical dependencies for a web application -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>${liquibase-core.version}</version>
</dependency>
</dependencies>
<!-- Package as an executable jar -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
We used the following dependencies:
spring-boot-starter-web - Spring Boot web container - used to create the REST API,
spring-boot-starter-data-jpa - Spring Data for JPA - used in persistence layer,
postgresql - PostgreSQL driver - used for connection with the PostgreSQL database,
liquibase - Liquibase to track, version, and deploy database changes.
3.2. Backend project structure
Let's take a look at the backend project file structure:
Copy
├── main
│ ├── java
│ │ └── com
│ │ └── frontbackend
│ │ └── springboot
│ │ ├── person
│ │ │ ├── config
│ │ │ │ └── CorsFilter.java
│ │ │ ├── controller
│ │ │ │ ├── model
│ │ │ │ │ ├── paging
│ │ │ │ │ │ ├── PageInfo.java
│ │ │ │ │ │ ├── Sorting.java
│ │ │ │ │ │ └── SortOrder.java
│ │ │ │ │ └── request
│ │ │ │ │ ├── CreatePersonRequest.java
│ │ │ │ │ ├── EditPersonRequest.java
│ │ │ │ │ └── PersonFilterRequest.java
│ │ │ │ └── PersonController.java
│ │ │ ├── dto
│ │ │ │ ├── PageDto.java
│ │ │ │ ├── PersonDto.java
│ │ │ │ └── PersonFilter.java
│ │ │ ├── exception
│ │ │ │ └── PersonNotFoundException.java
│ │ │ ├── jpa
│ │ │ │ ├── model
│ │ │ │ │ └── PersonEntity.java
│ │ │ │ ├── repository
│ │ │ │ │ └── PersonRepository.java
│ │ │ │ └── utils
│ │ │ │ ├── JpaSpecifications.java
│ │ │ │ └── PageInfoMapper.java
│ │ │ └── service
│ │ │ └── PersonService.java
│ │ └── SpringBootAngularNgZorroTable.java
│ └── resources
│ ├── application.properties
│ └── db-scripts
│ ├── db-changelog-master.xml
│ └── PERSON-1.0.0.xml
In that structure we could find the following classes:
SpringBootAngularNgZorroTable - the main Spring Boot class that starts the web container,
PersonController - REST controller class that will handle all HTTP requests,
PersonEntity - entity class that represents a table in the database,
PersonRepository - for communication with PostgreSQL database,
PersonService - service that calls repository methods,
JpaSpecifications - helper class used for filtering records,
CorsFilter - filter to allow REST API calls from different domains and port,
application.properties - Spring Boot configuration file.
3.3. Entity Model
The PersonEntity
class is a java representation of tbl_person
table in the PostgreSQL database:
Copy
package com.frontbackend.springboot.person.jpa.model;
import java.util.UUID;
import javax.persistence.Entity;
import javax.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.Type;
@Entity(name = "tbl_person")
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
public class PersonEntity {
@Id
@Type(type = "pg-uuid")
private UUID id;
private String firstName;
private String lastName;
private int age;
private String address;
}
The structure of the PersonEntity
class is simple. It contains three fields:
id
- the PRIMARY KEY (note that to handle UUID fields in JPA we need to add @Type(type="pg-uuid")
annotation,
firstName
- person first name ,
lastName
- person last name,
age
- person age,
address
- person address.
3.4. Persistance layer
The PersonRepository is an interface that extends Spring Data JpaRepository and JpaSpecificationExecutor :
Copy
package com.frontbackend.springboot.person.jpa.repository;
import com.frontbackend.springboot.person.jpa.model.PersonEntity;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface PersonRepository extends JpaRepository<PersonEntity, UUID>, JpaSpecificationExecutor<PersonEntity> {
}
This interface is used for CRUD operations on Person
object like save, update, delete, find etc.
3.5. JpaSpecifications class
This helper class will be used to filter records using JpaSpecification interface.
Copy
package com.frontbackend.springboot.person.jpa.utils;
import java.util.List;
import lombok.experimental.UtilityClass;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;
@UtilityClass
public class JpaSpecifications {
public <T> Specification<T> upperLike(String value, String columnName) {
return !StringUtils.hasText(value) ?
null :
((root, query, cb) -> cb.like(cb.upper(root.get(columnName)), "%" + value.toUpperCase() + "%"));
}
public <T> Specification<T> checkList(List<?> values, String columnName) {
return (values == null || values.isEmpty()) ?
null :
((root, query, cb) -> root.get(columnName)
.in(values));
}
}
3.6. PageInfo Mapper
The PageInfoMapper
class is a utility class used for mapping pagination requests that come from API into Spring PageRequest
object known by Spring Data
.
Copy
package com.frontbackend.springboot.person.jpa.utils;
import com.frontbackend.springboot.person.controller.model.paging.PageInfo;
import com.frontbackend.springboot.person.controller.model.paging.Sorting;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import lombok.experimental.UtilityClass;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.util.CollectionUtils;
@UtilityClass
public class PageInfoMapper {
public PageRequest toPageRequest(PageInfo pageInfo) {
return pageInfo.getSort() != null ? PageRequest.of(pageInfo.getPageNumber() - 1,
pageInfo.getPageSize(),
Sort.by(mapToOrders(pageInfo.getSort()))) :
PageRequest.of(pageInfo.getPageNumber() - 1, pageInfo.getPageSize());
}
private List<Sort.Order> mapToOrders(List<Sorting> sortingList) {
return CollectionUtils.isEmpty(sortingList) ? Collections.emptyList() :
sortingList.stream()
.map(PageInfoMapper::toOrder)
.collect(Collectors.toList());
}
private Sort.Order toOrder(Sorting sorting) {
return new Sort.Order(Sort.Direction.fromString(sorting.getOrder()
.name()), sorting.getColumn());
}
}
3.7. REST API
Spring Boot application will provide the following REST API:
URL
Method
Action
/api/persons/search
POST
Get page of filtered records
/api/persons/:id
GET
Find person by id
/api/persons/:id
PUT
Updating person data
/api/persons
POST
Create new person
/api/persons/:id
DELETE
Remove selected person
Above endpoints will be defined in PersonController
class annotated as Spring @RestController
:
Copy
package com.frontbackend.springboot.person.controller;
import com.frontbackend.springboot.person.controller.model.request.CreatePersonRequest;
import com.frontbackend.springboot.person.controller.model.request.PersonFilterRequest;
import com.frontbackend.springboot.person.dto.PageDto;
import com.frontbackend.springboot.person.dto.PersonDto;
import com.frontbackend.springboot.person.service.PersonService;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/persons")
public class PersonController {
private final PersonService personService;
@PostMapping
public UUID create(@RequestBody CreatePersonRequest createPersonRequest) {
return personService.save(PersonDto.builder()
.firstName(createPersonRequest.getFirstName())
.lastName(createPersonRequest.getLastName())
.age(createPersonRequest.getAge())
.address(createPersonRequest.getAddress())
.build());
}
@GetMapping("/{id}")
public PersonDto find(@PathVariable("id") UUID id) {
return personService.find(id);
}
@PutMapping("/{id}")
public UUID save(@PathVariable UUID id, @RequestBody CreatePersonRequest createPersonRequest) {
return personService.save(PersonDto.builder()
.id(id)
.firstName(createPersonRequest.getFirstName())
.lastName(createPersonRequest.getLastName())
.age(createPersonRequest.getAge())
.address(createPersonRequest.getAddress())
.build());
}
@DeleteMapping("/{id}")
public void delete(@PathVariable UUID id) {
personService.deletePerson(id);
}
@PostMapping("/search")
public PageDto<PersonDto> list(@RequestBody PersonFilterRequest request) {
return personService.filter(request.getFilter(), request.getPageInfo());
}
}
Controller endpoint methods used several POJO objects:
The request used for creating a new Person
:
Copy
package com.frontbackend.springboot.person.controller.model.request;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@NoArgsConstructor
@Setter
@Getter
public class CreatePersonRequest {
private String firstName;
private String lastName;
private int age;
private String address;
}
Request object used for Person
data modifications:
Copy
package com.frontbackend.springboot.person.controller.model.request;
import java.util.UUID;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@NoArgsConstructor
@Setter
@Getter
public class EditPersonRequest {
private UUID id;
private String firstName;
private String lastName;
private int age;
private String address;
}
The PersonFilterRequest
object contains:
PageInfo
- used for pagination and sorting results,
PersonFilter
- contains fields used for filtering records.
Copy
package com.frontbackend.springboot.person.controller.model.request;
import com.frontbackend.springboot.person.controller.model.paging.PageInfo;
import com.frontbackend.springboot.person.dto.PersonFilter;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@NoArgsConstructor
@Setter
@Getter
public class PersonFilterRequest {
private PageInfo pageInfo;
private PersonFilter filter;
}
PageInfo
holds pagination and sorting information:
Copy
package com.frontbackend.springboot.person.controller.model.paging;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class PageInfo {
private int pageNumber;
private int pageSize;
private List<Sorting> sort;
}
Sorting
class contains column and order fields:
Copy
package com.frontbackend.springboot.person.controller.model.paging;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Setter
@Getter
@NoArgsConstructor
public class Sorting {
private String column;
private SortOrder order;
}
SortOrder
is a simple enum object:
Copy
package com.frontbackend.springboot.person.controller.model.paging;
public enum SortOrder {
DESC,
ASC
}
PersonFilter
contains fields used for filtering rows:
Copy
package com.frontbackend.springboot.person.dto;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@NoArgsConstructor
@Getter
@Setter
public class PersonFilter {
private String firstName;
private String lastName;
private List<Integer> age;
private String address;
}
PageDto
wraps return records and contain additional totalPages
and totalElements
fields used by the table component on the front-end site:
Copy
package com.frontbackend.springboot.person.dto;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class PageDto<T> {
private final int totalPages;
private final long totalElements;
private final List<T> rows;
}
The PersonDto
is Data Transfer Object returned by REST API as JSON:
Copy
package com.frontbackend.springboot.person.dto;
import java.util.UUID;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class PersonDto {
private UUID id;
private String firstName;
private String lastName;
private int age;
private String address;
}
3.8. Service
PersonService
was introduced to eliminate complex methods in RestController
class. We can keep important logic here:
Copy
package com.frontbackend.springboot.person.service;
import static org.springframework.data.jpa.domain.Specification.where;
import com.frontbackend.springboot.person.controller.model.paging.PageInfo;
import com.frontbackend.springboot.person.dto.PageDto;
import com.frontbackend.springboot.person.dto.PersonDto;
import com.frontbackend.springboot.person.dto.PersonFilter;
import com.frontbackend.springboot.person.exception.PersonNotFoundException;
import com.frontbackend.springboot.person.jpa.model.PersonEntity;
import com.frontbackend.springboot.person.jpa.repository.PersonRepository;
import com.frontbackend.springboot.person.jpa.utils.JpaSpecifications;
import com.frontbackend.springboot.person.jpa.utils.PageInfoMapper;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class PersonService {
private final PersonRepository personRepository;
public UUID save(PersonDto personDto) {
return personDto.getId() != null ? saveModified(personDto) : saveNew(personDto);
}
public PersonDto find(UUID id) {
return personRepository.findById(id)
.map(this::toPersonDto)
.orElseThrow(PersonNotFoundException::new);
}
private UUID saveModified(PersonDto personDto) {
PersonEntity entity = personRepository.findById(personDto.getId())
.map(ent -> this.fillPersonEntity(personDto, ent))
.orElseThrow(PersonNotFoundException::new);
personRepository.save(entity);
return personDto.getId();
}
private UUID saveNew(PersonDto personDto) {
PersonEntity entity = fillPersonEntity(personDto, new PersonEntity());
entity.setId(UUID.randomUUID());
PersonEntity saved = personRepository.save(entity);
return saved.getId();
}
private PersonEntity fillPersonEntity(PersonDto personDto, PersonEntity entity) {
entity.setFirstName(personDto.getFirstName());
entity.setLastName(personDto.getLastName());
entity.setAddress(personDto.getAddress());
entity.setAge(personDto.getAge());
return entity;
}
public void deletePerson(UUID id) {
PersonEntity byId = personRepository.findById(id)
.orElseThrow(PersonNotFoundException::new);
personRepository.delete(byId);
}
public PageDto<PersonDto> filter(PersonFilter filter, PageInfo pageInfo) {
PageRequest pageRequest = PageInfoMapper.toPageRequest(pageInfo);
Page<PersonEntity> personEntityPage = personRepository.findAll(applyWhere(filter), pageRequest);
return PageDto.<PersonDto>builder()
.rows(personEntityPage.getContent()
.stream()
.map(this::toPersonDto)
.collect(Collectors.toList()))
.totalElements(personEntityPage.getTotalElements())
.totalPages(personEntityPage.getTotalPages())
.build();
}
private Specification<PersonEntity> applyWhere(PersonFilter filter) {
Specification<PersonEntity> firstName = JpaSpecifications.upperLike(filter.getFirstName(), "firstName");
Specification<PersonEntity> lastName = JpaSpecifications.upperLike(filter.getLastName(), "lastName");
Specification<PersonEntity> address = JpaSpecifications.upperLike(filter.getAddress(), "address");
Specification<PersonEntity> age = JpaSpecifications.checkList(filter.getAge(), "age");
return where(firstName).and(lastName)
.and(address)
.and(age);
}
private PersonDto toPersonDto(PersonEntity entity) {
return PersonDto.builder()
.id(entity.getId())
.firstName(entity.getFirstName())
.lastName(entity.getLastName())
.age(entity.getAge())
.address(entity.getAddress())
.build();
}
}
3.9. Spring Boot configuration file
In the Spring Boot configuration file we have properties responsible for the data source, JPA and Liquibase:
Copy
spring.datasource.url=jdbc:postgresql://localhost:5432/db
spring.datasource.username=username
spring.datasource.password=password
spring.liquibase.change-log=classpath:/db-scripts/db-changelog-master.xml
3.10. Liquibase
Liquibase
is used for managing database changes. To start using it in our Spring Boot project all we need to do is to add a special dependency in our pom.xml
file and create an entry in application.properties
file that points to change-log
:
Copy
spring.liquibase.change-log=classpath:/db-scripts/db-changelog-master.xml
The db-scripts/db-changelog-master.xml
has the following content:
Copy
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd">
<include file="db-scripts/PERSON-1.0.0.xml"/>
</databaseChangeLog>
As you can see it points to single file db-scripts/PERSON-1.0.0.xml
that contains command to create a tbl_person
table structure and fills it with data:
Copy
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd">
<changeSet author="frontbackend" id="1.0.0-1">
<createTable tableName="tbl_person">
<column name="id" type="uuid">
<constraints nullable="false"
primaryKey="true"
unique="true"/>
</column>
<column name="first_name" type="VARCHAR2(30)"/>
<column name="last_name" type="VARCHAR2(30)"/>
<column name="age" type="VARCHAR2(500)"/>
<column name="address" type="VARCHAR2(200)"/>
</createTable>
</changeSet>
<changeSet author="frontbackend" id="1.0.0-2">
<sql>
insert into tbl_person (id, first_name, last_name, age, address) values ('597f4585-aa32-4606-bcc5-ae070616de3b', 'Worden', 'Handes', 29, '4566 Di Loreto Park');
insert into tbl_person (id, first_name, last_name, age, address) values ('fb3013fb-5343-4832-ac95-1ee6f0fa264d', 'Eddie', 'Kivits', 48, '97525 Oakridge Trail');
...
insert into tbl_person (id, first_name, last_name, age, address) values ('25e55652-5a29-4a7a-94ba-38d960d45d9f', 'Lonnie', 'Gianni', 45, '578 Knutson Center');
</sql>
</changeSet>
</databaseChangeLog>
During the first Spring Boot run you should see in your console logs, similar to these below:
Copy
2022-05-21 22:17:52.988 INFO 101988 --- [ main] liquibase.database : Set default schema name to public
2022-05-21 22:17:53.048 INFO 101988 --- [ main] liquibase.lockservice : Successfully acquired change log lock
2022-05-21 22:17:53.179 INFO 101988 --- [ main] liquibase.changelog : Creating database history table with name: public.databasechangelog
2022-05-21 22:17:53.184 INFO 101988 --- [ main] liquibase.changelog : Reading from public.databasechangelog
Running Changeset: db-scripts/PERSON-1.0.0.xml::1.0.0-1::frontbackend
2022-05-21 22:17:53.222 INFO 101988 --- [ main] liquibase.changelog : Table tbl_person created
2022-05-21 22:17:53.223 INFO 101988 --- [ main] liquibase.changelog : ChangeSet db-scripts/PERSON-1.0.0.xml::1.0.0-1::frontbackend ran successfully in 6ms
Running Changeset: db-scripts/PERSON-1.0.0.xml::1.0.0-2::frontbackend
2022-05-21 22:17:53.267 INFO 101988 --- [ main] liquibase.changelog : Custom SQL executed
2022-05-21 22:17:53.268 INFO 101988 --- [ main] liquibase.changelog : ChangeSet db-scripts/PERSON-1.0.0.xml::1.0.0-2::frontbackend ran successfully in 42ms
2022-05-21 22:17:53.270 INFO 101988 --- [ main] liquibase.lockservice : Successfully released change log lock
This will be the approval that everything was well configured.
4. Angular application
To create an Angular application we used the Angular CLI, which is a command-line tool that can be used to initialize, develop, update, and maintain Angular applications directly from a system terminal.
In case you don't have Angular CLI on your machine, use the following command to install it globally:
Copy
npm install -g @angular/cli
Next, let's create an Angular project called table
:
Copy
ng new table
Angular CLI will print the following output:
Copy
Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS [ https://sass-lang.com/documentation/syntax#scss
]
CREATE table/README.md (1051 bytes)
CREATE table/.editorconfig (274 bytes)
CREATE table/.gitignore (548 bytes)
CREATE table/angular.json (3201 bytes)
CREATE table/package.json (1068 bytes)
CREATE table/tsconfig.json (863 bytes)
CREATE table/.browserslistrc (600 bytes)
CREATE table/karma.conf.js (1422 bytes)
CREATE table/tsconfig.app.json (287 bytes)
CREATE table/tsconfig.spec.json (333 bytes)
CREATE table/.vscode/extensions.json (130 bytes)
CREATE table/.vscode/launch.json (474 bytes)
CREATE table/.vscode/tasks.json (938 bytes)
CREATE table/src/favicon.ico (948 bytes)
CREATE table/src/index.html (291 bytes)
CREATE table/src/main.ts (372 bytes)
CREATE table/src/polyfills.ts (2338 bytes)
CREATE table/src/styles.scss (80 bytes)
CREATE table/src/test.ts (745 bytes)
CREATE table/src/assets/.gitkeep (0 bytes)
CREATE table/src/environments/environment.prod.ts (51 bytes)
CREATE table/src/environments/environment.ts (658 bytes)
CREATE table/src/app/app-routing.module.ts (245 bytes)
CREATE table/src/app/app.module.ts (393 bytes)
CREATE table/src/app/app.component.scss (0 bytes)
CREATE table/src/app/app.component.html (23364 bytes)
CREATE table/src/app/app.component.spec.ts (1070 bytes)
CREATE table/src/app/app.component.ts (210 bytes)
✔ Packages installed successfully.
In the next step we added ng-zorro-antd
dependency to our Angular project:
Copy
ng add ng-zorro-antd
The output of the above command will be as follows:
Copy
ℹ Using package manager: npm
✔ Found compatible package version: ng-zorro-antd@13.2.2.
✔ Package information loaded.
The package ng-zorro-antd@13.2.2 will be installed and executed.
Would you like to proceed? Yes
✔ Package successfully installed.
? Enable icon dynamic loading [ Detail: https://ng.ant.design/components/icon/en ] No
? Set up custom theme file [ Detail: https://ng.ant.design/docs/customize-theme/en ] No
? Choose your locale code: en_US
? Choose template to create project: sidemenu
CREATE src/app/icons-provider.module.ts (482 bytes)
CREATE src/app/pages/welcome/welcome-routing.module.ts (352 bytes)
CREATE src/app/pages/welcome/welcome.component.scss (0 bytes)
CREATE src/app/pages/welcome/welcome.component.html (0 bytes)
CREATE src/app/pages/welcome/welcome.component.ts (274 bytes)
CREATE src/app/pages/welcome/welcome.module.ts (314 bytes)
UPDATE package.json (1099 bytes)
UPDATE src/app/app.module.ts (1127 bytes)
UPDATE angular.json (3337 bytes)
UPDATE src/app/app-routing.module.ts (416 bytes)
UPDATE src/app/app.component.scss (1245 bytes)
UPDATE src/app/app.component.html (1592 bytes)
UPDATE src/app/app.component.ts (214 bytes)
✔ Packages installed successfully.
Now, we can run the Angular application to see if everything works as expected:
Type this command in your terminal to start the Angular app:
Copy
ng serve
Go to the browser and enter: http://localhost:4200
into the address bar. You should see the following screen:
Of course, this is not what our final application will look like :)
4.1. Table module and component
First, we need to create a new module and base table component:
Copy
ng g module pages/table
ng g component pages/table
The remaining objects will be created manually.
The final Angular project has the following structure:
Copy
.
├── app
│ ├── app.component.html
│ ├── app.component.scss
│ ├── app.component.spec.ts
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── app-routing.module.ts
│ ├── components
│ │ └── person-form-modal
│ │ ├── person-form-modal.component.html
│ │ ├── person-form-modal.component.scss
│ │ └── person-form-modal.component.ts
│ ├── icons-provider.module.ts
│ ├── model
│ │ ├── page.info.ts
│ │ ├── page.response.ts
│ │ ├── person.filter.ts
│ │ ├── person.page.request.ts
│ │ ├── person.ts
│ │ ├── sorting.ts
│ │ └── sort.order.ts
│ ├── pages
│ │ ├── table
│ │ │ ├── table.component.html
│ │ │ ├── table.component.scss
│ │ │ ├── table.component.spec.ts
│ │ │ ├── table.component.ts
│ │ │ ├── table.module.ts
│ │ │ └── table-routing.module.ts
│ │ └── welcome
│ │ ├── welcome.component.html
│ │ ├── welcome.component.scss
│ │ ├── welcome.component.ts
│ │ ├── welcome.module.ts
│ │ └── welcome-routing.module.ts
│ └── service
│ └── person.http.service.ts
├── assets
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── favicon.ico
├── index.html
├── main.ts
├── polyfills.ts
├── styles.scss
└── test.ts
4.2. Table module and components
Table module will be used to aggregate all components related to table
.
Copy
import { Component, OnInit } from '@angular/core';
import { Person } from '../../model/person';
import { NzTableQueryParams } from 'ng-zorro-antd/table';
import { PersonPageRequest } from '../../model/person.page.request';
import { PersonFilter } from '../../model/person.filter';
import { PageInfo } from '../../model/page.info';
import { SortOrder } from '../../model/sort.order';
import { PersonHttpService } from '../../service/person.http.service';
import { NzModalService } from 'ng-zorro-antd/modal';
import { PersonFormModalComponent } from '../../components/person-form-modal/person-form-modal.component';
@Component({
selector: 'app-table',
templateUrl: './table.component.html',
styleUrls: ['./table.component.scss']
})
export class TableComponent implements OnInit {
persons: Person[] = [];
loading = true;
pageSize = 10;
pageIndex = 1;
total = 1;
visibleFn = false;
visibleLn = false;
pageRequest: PersonPageRequest = {
pageInfo: {
pageSize: this.pageSize,
pageNumber: this.pageIndex,
sort: null
} as PageInfo,
filter: {
firstName: '',
lastName: ''
} as PersonFilter
};
constructor(private personService: PersonHttpService,
private modal: NzModalService) {
}
ngOnInit(): void {
}
onQueryParamsChange(params: NzTableQueryParams): void {
const {pageSize, pageIndex, sort, filter} = params;
const currentSort = sort.find(item => item.value !== null);
const sortField = (currentSort && currentSort.key) || null;
const sortOrder = (currentSort && currentSort.value) || null;
this.pageRequest.pageInfo = {
pageSize: pageSize,
pageNumber: pageIndex,
sort: (sortField && sortOrder) ? [
{
column: sortField,
order: sortOrder === 'ascend' ? SortOrder.ASC : SortOrder.DESC
}
] : null
};
this.search();
}
resetFirstName(): void {
this.pageRequest.filter.firstName = '';
this.search();
}
resetLastName(): void {
this.pageRequest.filter.lastName = '';
this.search();
}
search(): void {
this.visibleFn = false;
this.visibleLn = false;
this.loading = true;
this.personService.personPage(this.pageRequest).subscribe(result => {
this.persons = result.rows;
this.total = result.totalElements;
this.loading = false;
}, () => this.loading = false);
}
addPerson(): void {
this.modal.create({
nzContent: PersonFormModalComponent,
nzWidth: 900,
nzCentered: true,
nzComponentParams: {
person: {} as Person
} as Partial<PersonFormModalComponent>
}).afterClose.subscribe(data => {
if (!!data) {
this.search();
}
});
}
removePerson(id: string): void {
this.personService.delete(id).subscribe(result => {
this.search();
})
}
editPerson(id: string): void {
this.personService.find(id).subscribe(result => {
this.modal.create({
nzContent: PersonFormModalComponent,
nzWidth: 900,
nzCentered: true,
nzComponentParams: {
person: result
} as Partial<PersonFormModalComponent>
}).afterClose.subscribe(data => {
if (!!data) {
this.search();
}
});
});
}
}
Table component HTML file:
Copy
<button nz-button (click)="addPerson()" nzType="primary">Add</button>
<br/>
<br/>
<nz-table
nzShowSizeChanger
[nzData]="persons"
[nzFrontPagination]="false"
[nzLoading]="loading"
[nzTotal]="total"
[nzPageSize]="pageSize"
[nzPageIndex]="pageIndex"
(nzQueryParams)="onQueryParamsChange($event)"
>
<thead>
<tr>
<th nzColumnKey="firstName" [nzSortFn]="true" nzCustomFilter>
First name
<nz-filter-trigger [(nzVisible)]="visibleFn" [nzActive]="pageRequest.filter.firstName.length > 0" [nzDropdownMenu]="menuFn">
<i nz-icon nzType="search"></i>
</nz-filter-trigger>
<nz-dropdown-menu #menuFn="nzDropdownMenu">
<div class="ant-table-filter-dropdown">
<div class="search-box">
<input type="text" nz-input placeholder="Search person" [(ngModel)]="pageRequest.filter.firstName"/>
<button nz-button nzSize="small" nzType="primary" (click)="search()" class="search-button">Search</button>
<button nz-button nzSize="small" (click)="resetFirstName()">Reset</button>
</div>
</div>
</nz-dropdown-menu>
</th>
<th nzColumnKey="lastName" [nzSortFn]="true" nzCustomFilter>
Last name
<nz-filter-trigger [(nzVisible)]="visibleLn" [nzActive]="pageRequest.filter.lastName.length > 0" [nzDropdownMenu]="menuLn">
<i nz-icon nzType="search"></i>
</nz-filter-trigger>
<nz-dropdown-menu #menuLn="nzDropdownMenu">
<div class="ant-table-filter-dropdown">
<div class="search-box">
<input type="text" nz-input placeholder="Search person" [(ngModel)]="pageRequest.filter.lastName"/>
<button nz-button nzSize="small" nzType="primary" (click)="search()" class="search-button">Search</button>
<button nz-button nzSize="small" (click)="resetLastName()">Reset</button>
</div>
</div>
</nz-dropdown-menu>
</th>
<th nzColumnKey="age" [nzSortFn]="true">Age</th>
<th nzColumnKey="address" [nzSortFn]="true">Address</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let data of persons">
<td>{{ data.firstName }}</td>
<td>{{ data.lastName }}</td>
<td>{{ data.age }}</td>
<td>{{ data.address }}</td>
<td>
<nz-space>
<button *nzSpaceItem nz-button nzType="primary" (click)="editPerson(data.id)"><i nz-icon nzType="edit"></i></button>
<button *nzSpaceItem nz-button nzDanger nzType="primary" nz-popconfirm nzPopconfirmTitle="Sure to delete?" (nzOnConfirm)="removePerson(data.id)">
<i nz-icon nzType="delete"></i>
</button>
</nz-space>
</td>
</tr>
</tbody>
</nz-table>
Table component module, where we import several NgZorro
modules:
NzTableModule - module for NgZorro Table
component,
NzFormModule - the NgZorro
form module,
NzButtonModule - the NgZorro
buttons module,
NzInputModule - NgZorro
input module,
NzIconModule - module for icons,
NzDropDownModule - module for drop-down components,
NzPopconfirmModule - component for inline confirmation popups,
NzModalModule - modal components,
NzSpaceModule - spaces between components.
Copy
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TableComponent } from './table.component';
import { TableRoutingModule } from './table-routing.module';
import { NzTableModule } from 'ng-zorro-antd/table';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm';
import { NzSpaceModule } from 'ng-zorro-antd/space';
import { NzModalModule } from 'ng-zorro-antd/modal';
import { PersonFormModalComponent } from '../../components/person-form-modal/person-form-modal.component';
import { NzFormModule } from 'ng-zorro-antd/form';
@NgModule({
declarations: [
TableComponent,
PersonFormModalComponent
],
imports: [
CommonModule,
TableRoutingModule,
FormsModule,
ReactiveFormsModule,
NzTableModule,
NzIconModule,
NzDropDownModule,
NzInputModule,
NzButtonModule,
NzPopconfirmModule,
NzSpaceModule,
NzModalModule,
NzFormModule
]
})
export class TableModule {
}
The table routing module is simple and contains a mapping for the root
table path:
Copy
import { RouterModule, Routes } from '@angular/router';
import { NgModule } from '@angular/core';
import { TableComponent } from './table.component';
const routes: Routes = [
{path: '', component: TableComponent},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class TableRoutingModule {
}
To create a new person and modify the existing one we use the form presented in the NgZorro
modal:
Copy
import { Component, Input, OnInit } from '@angular/core';
import { NzModalRef } from 'ng-zorro-antd/modal';
import { Person } from '../../model/person';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { PersonHttpService } from '../../service/person.http.service';
@Component({
selector: 'app-person-form-modal',
templateUrl: './person-form-modal.component.html',
styleUrls: ['./person-form-modal.component.scss']
})
export class PersonFormModalComponent implements OnInit {
@Input()
person!: Person;
personForm!: FormGroup;
constructor(private modalRef: NzModalRef,
private personService: PersonHttpService,
private fb: FormBuilder) {
}
ngOnInit(): void {
this.personForm = this.fb.group({
id: [null],
firstName: [null, [Validators.required]],
lastName: [null, [Validators.required]],
age: [null, [Validators.required]],
address: [null, [Validators.required]]
});
if (this.person && this.person.id) {
this.personForm.setValue(this.person);
}
}
closeModal(): void {
this.modalRef.destroy();
}
submitForm(): void {
for(const i in this.personForm.controls) {
if (this.personForm.controls.hasOwnProperty(i)) {
this.personForm.controls[i].markAsDirty();
this.personForm.controls[i].updateValueAndValidity();
}
}
if (this.personForm.valid) {
if (this.person.id) {
this.personService.save(this.personForm.value).subscribe(result => {
this.modalRef.destroy(result);
});
} else {
this.personService.create(this.personForm.value).subscribe(result => {
this.modalRef.destroy(result);
});
}
}
}
}
The HTML file with form
component:
Copy
<div *nzModalTitle>
Person
</div>
<div>
<form id="personForm" nz-form [formGroup]="personForm" (ngSubmit)="submitForm()">
<nz-form-item>
<nz-form-label [nzSm]="6" nzFor="firstNameField" nzRequired>First name</nz-form-label>
<nz-form-control [nzSm]="16">
<input id="firstNameField" nz-input formControlName="firstName" autocomplete="off"/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSm]="6" nzFor="lastNameField" nzRequired>Last name</nz-form-label>
<nz-form-control [nzSm]="16">
<input id="lastNameField" nz-input formControlName="lastName" autocomplete="off"/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSm]="6" nzFor="ageField" nzRequired>Age</nz-form-label>
<nz-form-control [nzSm]="16">
<input id="ageField" nz-input formControlName="age" autocomplete="off"/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSm]="6" nzFor="addressField" nzRequired>Address</nz-form-label>
<nz-form-control [nzSm]="16">
<input id="addressField" nz-input formControlName="address" autocomplete="off"/>
</nz-form-control>
</nz-form-item>
</form>
</div>
<div *nzModalFooter>
<button nz-button nzType="default" (click)="closeModal()">Close</button>
<button type="submit" form="personForm" nz-button nzType="primary" (click)="submitForm()">Save</button>
</div>
4.4. HTTP client
The PersonHttpService
using the HTTPClient
library for communication with backend REST API:
Copy
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { PersonPageRequest } from '../model/person.page.request';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
import { Person } from '../model/person';
import { PageResponse } from '../model/page.response';
const BASE_URL = environment.baseUrl;
@Injectable({
providedIn: 'root'
})
export class PersonHttpService {
constructor(private http: HttpClient) {
}
personPage(request: PersonPageRequest): Observable<PageResponse<Person>> {
return this.http.post<PageResponse<Person>>(`${BASE_URL}/api/persons/search`, request);
}
find(id: string): Observable<Person> {
return this.http.get<Person>(`${BASE_URL}/api/persons/${id}`);
}
delete(id: string): Observable<any> {
return this.http.delete<Person>(`${BASE_URL}/api/persons/${id}`);
}
save(request: Person): Observable<any> {
return this.http.put<Person>(`${BASE_URL}/api/persons/${request.id}`, request);
}
create(request: Person): Observable<any> {
return this.http.post<Person>(`${BASE_URL}/api/persons`, request);
}
}
Helper typescript objects that have representation in Java project:
Page info interface:
Copy
import { Sorting } from './sorting';
export interface PageInfo {
pageNumber: number;
pageSize: number;
sort: Sorting[] | null;
}
Sort order enum:
Copy
export enum SortOrder {
ASC = 'ASC',
DESC = 'DESC'
}
Sorting interface:
Copy
import { SortOrder } from './sort.order';
export interface Sorting {
order: SortOrder;
column: string;
}
Person interface:
Copy
export interface Person {
id: string;
firstName: string;
lastName: string;
age: number;
address: string;
}
Person page request with filter and page info object:
Copy
import { PageInfo } from './page.info';
import { PersonFilter } from './person.filter';
export interface PersonPageRequest {
pageInfo: PageInfo;
filter: PersonFilter;
}
Person filter interface:
Copy
export interface PersonFilter {
firstName: string;
lastName: string;
}
Person response object with rows and total elements/pages info:
Copy
export interface PageResponse<T> {
rows: T[];
totalElements: number;
totalPages: number;
}
4.5. Base application component
The AppComponent
will be our main component.
Copy
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
isCollapsed = false;
}
The main component contains sidebar menu with a Welcome
and Table
items:
Copy
<nz-layout class="app-layout">
<nz-sider class="menu-sidebar"
nzCollapsible
nzWidth="256px"
nzBreakpoint="md"
[(nzCollapsed)]="isCollapsed"
[nzTrigger]="null">
<div class="sidebar-logo">
<a href="https://ng.ant.design/" target="_blank">
<img src="https://ng.ant.design/assets/img/logo.svg" alt="logo">
<h1>Ant Design Of Angular</h1>
</a>
</div>
<ul nz-menu nzTheme="dark" nzMode="inline" [nzInlineCollapsed]="isCollapsed">
<li nz-submenu nzOpen nzTitle="Dashboard" nzIcon="dashboard">
<ul>
<li nz-menu-item nzMatchRouter>
<a routerLink="/welcome">Welcome</a>
</li>
<li nz-menu-item nzMatchRouter>
<a routerLink="/table">Table</a>
</li>
</ul>
</li>
</ul>
</nz-sider>
<nz-layout>
<nz-header>
<div class="app-header">
<span class="header-trigger" (click)="isCollapsed = !isCollapsed">
<i class="trigger"
nz-icon
[nzType]="isCollapsed ? 'menu-unfold' : 'menu-fold'"
></i>
</span>
</div>
</nz-header>
<nz-content>
<div class="inner-content">
<router-outlet></router-outlet>
</div>
</nz-content>
</nz-layout>
</nz-layout>
4.6. Application Module and Routing
The main application module imports all necessary modules from core Angular and NgZorro
library:
Copy
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { en_US, NZ_I18N } from 'ng-zorro-antd/i18n';
import { registerLocaleData } from '@angular/common';
import en from '@angular/common/locales/en';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { IconsProviderModule } from './icons-provider.module';
import { NzLayoutModule } from 'ng-zorro-antd/layout';
import { NzMenuModule } from 'ng-zorro-antd/menu';
import { NzFormModule } from 'ng-zorro-antd/form';
registerLocaleData(en);
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
HttpClientModule,
BrowserAnimationsModule,
IconsProviderModule,
NzLayoutModule,
NzMenuModule
],
providers: [{provide: NZ_I18N, useValue: en_US}],
bootstrap: [AppComponent]
})
export class AppModule {
}
In the routing module we defined two pages:
welcome
- redirects to the blank page,
table
- redirect to page with table component.
Copy
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: '/welcome' },
{ path: 'welcome', loadChildren: () => import('./pages/welcome/welcome.module').then(m => m.WelcomeModule) },
{ path: 'table', loadChildren: () => import('./pages/table/table.module').then(m => m.TableModule) }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
4.6. Environment file
In environment
file we added baseUrl
that points to Spring Boot application server:
Copy
export const environment = {
production: false,
baseUrl: 'http://localhost:8080'
};
5. Demo
Blank page with side-bar menu:
/table
page with NgZorro
table component:
Creating the new person in modal:
Removing entry requires inline confirmation:
We can filter by columns:
Menu could be collapsed:
6. Conclusion
In this article, we presented how to create a Spring Boot application connected with the PostgreSQL database and Angular on the frontend site. The NgZorro Table
component was introduced with several features like sorting, filtering, removing (with inline confirmation), editing, and creating.
As usual this example could be found on our GitHub repository: GitHub
{{ 'Comments (%count%)' | trans {count:count} }}
{{ 'Comments are closed.' | trans }}