Spring Boot + PostgreSQL + Angular NgZorro Mention

September 27, 2022 No comments Angular NgZorro Mention Spring Boot PostgreSQL

Spring Boot + PostgreSQL + Angular NgZorro Autocomplete

1. Introduction

The NgZorro is an enterprise-class Angular UI component library that we can use in our applications under the MIT license. This article will show you how to use NgZorro Mention Component with Spring Boot and PostgreSQL database.

2. Overview of an example application

The example application will use Angular as a frontend framework, Spring Boot to handle REST requests, and PostgreSQL database to persist any submitted data. Mention Component is often used when we need to mention someone or something in the comment or message. In this article, we will present how to easily integrate it into our application.

3. Spring boot application

Let's start with the back-end site of the application. We will use Spring Boot as our main server that will provide an API for communication with the front-end site. We will use REST communication based on JSON.

3.1. Maven dependencies

First, we need to create a Maven project, with the following pom.xml file:

<?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-mention</artifactId>
    <!-- Inherit defaults from Spring Boot -->

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.1</version>
        <relativePath/>
    </parent>

    <!-- 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>1.18.24</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 several dependecies:

  • 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 database.
3.2. Backend project structure

Let's take a look at the backend project file structure:

├── main
│   ├── java
│   │   └── com
│   │       └── frontbackend
│   │           └── springboot
│   │               ├── Application.java
│   │               ├── config
│   │               │   └── CorsFilter.java
│   │               └── users
│   │                   ├── SaveTopUsersRequest.java
│   │                   ├── UserController.java
│   │                   ├── UserEntity.java
│   │                   ├── UserRepository.java
│   │                   └── UserService.java
│   └── resources
│       ├── application.properties
│       └── insert.sql
└── test
    └── java

In that structure we could find the following classes:

  • Application - this is the main Spring Boot class that starts the web container,
  • UserController - REST controller class that will handle all HTTP requests,
  • UserEntity - entity class that represents object in the database,
  • UserRepository - for communication with database,
  • UserService - aggregates all business logic,
  • CorsFilter - use to give Angular application permission to call REST API on the different port,
  • application.properties - Spring Boot configuration file,
  • insert.sql - SQL inserts with sample users.
3.3. Model

The UserEntity class is a java representation of tbl_user table in the PostgreSQL database:

package com.frontbackend.springboot.users;

import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Getter
@Setter
@Table(name = "tbl_user")
public class UserEntity {

    @Id
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name = "uuid", strategy = "uuid2")
    private String id;

    private String username;
    private Boolean top;
}

The structure of the UserEntity class is simple. It contains three fields:

  • id - the PRIMARY KEY,
  • username - String with username,
  • top - a flag tells if a user is at the top.
3.4. Persistance layer

The UserRepository extends Spring Data JpaRepository and contains some additional query methods:

package com.frontbackend.springboot.users;

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<UserEntity, String> {

    List<UserEntity> findByUsernameStartsWith(String searchFilter);

    List<UserEntity> findAllByTopIsTrue();
}
3.5. REST API

Spring Boot application will provide the following REST API:

URL Method Action
/api/users GET Get list of all users
/api/users/top GET List of only TOP users
/api/users/top POST Updating list of TOP users

All endpoints will be defined in UserController class marked as Spring @RestController:

package com.frontbackend.springboot.users;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    @GetMapping
    public List<String> findUsernames(@RequestParam(value = "filter", required = false) String filter) {
        return userService.findUsernames(filter);
    }

    @GetMapping("/top")
    public List<String> getTopUsernames() {
        return userService.getTopUsernames();
    }

    @PostMapping("/top")
    public void top(@RequestBody SaveTopUsersRequest request) {
        userService.saveTopUsers(request.getUsernames());
    }
}
3.6. Service

Basic business logic is located in the UserService class. This service allows us to aggregate methods with complex logic away from the Controller class.

package com.frontbackend.springboot.users;

import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;

    public List<String> findUsernames(String filter) {
        if (StringUtils.hasText(filter)) {
            return toUsernames(userRepository.findByUsernameStartsWith(filter));
        }
        return toUsernames(userRepository.findAll());
    }

    private List<String> toUsernames(List<UserEntity> userEntities) {
        return userEntities.stream()
                .map(UserEntity::getUsername)
                .collect(Collectors.toList());
    }

    @Transactional
    public void saveTopUsers(List<String> topUsernames) {
        userRepository.findAll()
                .forEach(user -> user.setTop(topUsernames.contains(user.getUsername())));
    }

    public List<String> getTopUsernames() {
        return userRepository.findAllByTopIsTrue()
                .stream()
                .map(UserEntity::getUsername)
                .collect(Collectors.toList());
    }
}
3.7. Spring Boot configuration file

In the Spring Boot configuration file we have properties responsible for the data source and JPA:

spring.datasource.url=jdbc:postgresql://localhost:5432/db
spring.datasource.username=username
spring.datasource.password=password

spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

spring.jpa.hibernate.ddl-auto=update
3.8. Insert test data

After we run the Spring Boot application for the first time, we will need to insert some example data:

Run insert.sql script:

INSERT INTO public.tbl_user (id, username, top) VALUES ('0dd816d9-bdfd-49a6-985b-343406711c8e', 'john12', false);
INSERT INTO public.tbl_user (id, username, top) VALUES ('feb0581e-3b1c-443d-8acc-adcfb4564a8f', 'gokart99', false);
INSERT INTO public.tbl_user (id, username, top) VALUES ('c745d1c1-cb75-4bff-8804-f5911089b54e', 'testbong', false);
INSERT INTO public.tbl_user (id, username, top) VALUES ('e364c118-5408-4571-9bcb-d432466acfb9', 'rjeik', false);
INSERT INTO public.tbl_user (id, username, top) VALUES ('7f2cf07c-da9c-49a9-bac1-4c9632ebfa60', 'rooter33', false);

4. Angular application

The easiest way to create an Angular application is to use the Angular CLI, which is a command-line tool that you use to initialize, develop, update, and maintain Angular applications directly from a terminal.

In case you don't have Angular CLI on your machine, use the following to install it globally:

npm install -g @angular/cli

Next, let's create our example application called mention:

ng new mention

System should print the following output:

? 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 mention/README.md (1053 bytes)
CREATE mention/.editorconfig (274 bytes)
CREATE mention/.gitignore (548 bytes)
CREATE mention/angular.json (3213 bytes)
CREATE mention/package.json (1070 bytes)
CREATE mention/tsconfig.json (863 bytes)
CREATE mention/.browserslistrc (600 bytes)
CREATE mention/karma.conf.js (1424 bytes)
CREATE mention/tsconfig.app.json (287 bytes)
CREATE mention/tsconfig.spec.json (333 bytes)
CREATE mention/.vscode/extensions.json (130 bytes)
CREATE mention/.vscode/launch.json (474 bytes)
CREATE mention/.vscode/tasks.json (938 bytes)
CREATE mention/src/favicon.ico (948 bytes)
CREATE mention/src/index.html (293 bytes)
CREATE mention/src/main.ts (372 bytes)
CREATE mention/src/polyfills.ts (2338 bytes)
CREATE mention/src/styles.scss (80 bytes)
CREATE mention/src/test.ts (745 bytes)
CREATE mention/src/assets/.gitkeep (0 bytes)
CREATE mention/src/environments/environment.prod.ts (51 bytes)
CREATE mention/src/environments/environment.ts (658 bytes)
CREATE mention/src/app/app-routing.module.ts (245 bytes)
CREATE mention/src/app/app.module.ts (393 bytes)
CREATE mention/src/app/app.component.scss (0 bytes)
CREATE mention/src/app/app.component.html (23364 bytes)
CREATE mention/src/app/app.component.spec.ts (1076 bytes)
CREATE mention/src/app/app.component.ts (212 bytes)
✔ Packages installed successfully.

The next step will be to add ng-zorro-antd dependency to our Angular project:

ng add ng-zorro-antd

The output of above command will me as follows:

ℹ Using package manager: npm
✔ Found compatible package version: ng-zorro-antd@13.1.1.
✔ Package information loaded.

The package ng-zorro-antd@13.1.1 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: blank
UPDATE package.json (1101 bytes)
UPDATE src/app/app.module.ts (895 bytes)
UPDATE angular.json (3349 bytes)
UPDATE src/app/app.component.html (276 bytes)
✔ Packages installed successfully.

Now, we could run the Angular application to see if everything works as expected:

ng serve

Go to the browser and enter: http://localhost:4200 into the address bar. You should see the following screen:

Ng zorro welcome screen

Of course, this is not our target application, we need to build it.

4.1. Angular project structure
.
├── app
│   ├── app.component.html
│   ├── app.component.scss
│   ├── app.component.spec.ts
│   ├── app.component.ts
│   ├── app.module.ts
│   ├── app-routing.module.ts
│   ├── user-http.service.spec.ts
│   └── user-http.service.ts
├── assets
├── environments
│   ├── environment.prod.ts
│   └── environment.ts
├── favicon.ico
├── index.html
├── main.ts
├── polyfills.ts
├── styles.scss
└── test.ts

The structure is simple:

  • app.component.html - this is our main component,
  • app.component.scss - styles used in main app component,
  • app.component.ts - main app component TypeScript class,
  • app.module.ts - base application module,
  • user-http.service.ts - service with Http client used for communication with REST API.
4.2. HTTP client

The UserHttpService using the HTTPClient library for communication with backend:

import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../environments/environment';

const baseUrl = `${environment.baseUrl}`;

@Injectable({
  providedIn: 'root'
})
export class UserHttpService {

  constructor(private http: HttpClient) {
  }

  list(filter: string): Observable<string[]> {
    let queryParams = new HttpParams();
    queryParams.set('filter', filter);
    return this.http.get<string[]>(`${baseUrl}/api/users`, {params: queryParams});
  }

  save(list: string[]): Observable<any> {
    return this.http.post(`${baseUrl}/api/users/top`, {usernames: list});
  }

  getTopUsers(): Observable<string[]> {
    return this.http.get<string[]>(`${baseUrl}/api/users/top`);
  }
}
4.3. Base application component

The AppComponent will be our main component responsible for interactions with the end-user.

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MentionOnSearchTypes } from 'ng-zorro-antd/mention';
import { UserHttpService } from './user-http.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

  title = 'mention';
  validateForm!: FormGroup;
  inputValue?: string;
  loading = false;
  topUsers: string[] = [];

  constructor(private fb: FormBuilder,
              private userHttpService: UserHttpService) {
  }

  ngOnInit(): void {
    this.validateForm = this.fb.group({
      mention: [null, [Validators.required]]
    });

    this.getTop();
  }

  private getTop() {
    this.userHttpService.getTopUsers().subscribe(topUsers => {
      this.topUsers = topUsers;
    }, () => alert('An error occurred while retrieving top usernames'));
  }

  submitForm(): void {
    Object.keys(this.validateForm.controls).forEach(field => {
      const control = this.validateForm.get(field);
      control?.markAsTouched({onlySelf: true});
    });
    if (this.validateForm.dirty && this.validateForm.valid) {
      let mention: any = this.validateForm.value.mention;
      let list = mention ? mention.split('@').filter((u: string) => u != '').map((u: string) => u.trim()) : [];
      this.userHttpService.save(list).subscribe(found => {
        this.getTop();
      }, () => {
        alert('An error occurred!');
      });
    }
  }

  resetForm(): void {
    this.validateForm?.reset();
  }

  suggestions: string[] = [];

  onSearchChange({value}: MentionOnSearchTypes): void {
    this.loading = true;
    this.fetchSuggestions(value, suggestions => {
      this.suggestions = suggestions;
      this.loading = false;
    });
  }

  fetchSuggestions(value: string, callback: (suggestions: string[]) => void): void {
    this.userHttpService.list(value).subscribe(found => {
      callback(found);
    });
  }
}
4.4. Base component HTML file

The main component allows us to select mention users, using NgZorro Mention component, and submits selected values. Submitted usernames will be marked as TOP in the database.

<div nz-row>
  <div nz-col nzSpan="12" nzOffset="6">
    <form nz-form [formGroup]="validateForm" (ngSubmit)="submitForm()">
      <nz-form-item>
        <nz-form-label [nzSm]="6" nzFor="mention">Top programmers</nz-form-label>
        <nz-form-control [nzSm]="16" nzErrorTip="At least one must be selected!">
          <nz-mention [nzSuggestions]="suggestions" [nzLoading]="loading" (nzOnSearchChange)="onSearchChange($event)">
            <input nzMentionTrigger placeholder="input here" nz-input formControlName="mention" nz-input/>
          </nz-mention>
        </nz-form-control>
      </nz-form-item>
      <nz-form-item nz-row style="margin-bottom:8px;">
        <nz-form-control [nzSpan]="14" [nzOffset]="6">
          <button type="button" nz-button nzType="primary" (click)="submitForm()">Submit</button>
          &nbsp;&nbsp;&nbsp;
          <button type="button" nz-button (click)="resetForm()">Reset</button>
        </nz-form-control>
      </nz-form-item>
    </form>
  </div>
</div>
<div nz-row *ngIf="topUsers && topUsers.length > 0">
  <div nz-col nzSpan="12" nzOffset="6">
    <h2>Top users</h2>
    <ul>
      <li *ngFor="let user of topUsers">{{user}}</li>
    </ul>
  </div>
</div>
4.5. Application Module

In main Angular module we need to import several modules:

  • NzMentionModule - module for NgZorro Mention component,
  • NzFormModule - the NgZorro form module,
  • NzButtonModule - the NgZorro buttons module,
  • NzInputModule - NgZorro input module.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { NZ_I18N } from 'ng-zorro-antd/i18n';
import { en_US } from 'ng-zorro-antd/i18n';
import { registerLocaleData } from '@angular/common';
import en from '@angular/common/locales/en';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {NzMentionModule} from "ng-zorro-antd/mention";
import {NzFormModule} from "ng-zorro-antd/form";
import {NzButtonModule} from "ng-zorro-antd/button";
import { NzInputModule } from 'ng-zorro-antd/input';

registerLocaleData(en);

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    HttpClientModule,
    BrowserAnimationsModule,
    ReactiveFormsModule,
    NzMentionModule,
    NzFormModule,
    NzButtonModule,
    NzInputModule
  ],
  providers: [{ provide: NZ_I18N, useValue: en_US }],
  bootstrap: [AppComponent]
})
export class AppModule { }
4.6. Environment file

In environment file we added baseUrl that points to Spring Boot application server:

export const environment = {
  production: false,
  baseUrl: 'http://localhost:8080'
};

5. Demo

Spring boot postgresql angular ng zorro mention

6. Conclusion

In this article, we presented how to create Spring Boot application with PostgreSQL database and Angular on the frontend site. We show how to use NgZorro Mention component.

As usual this example could be found on our GitHub repository: GitHub

{{ message }}

{{ 'Comments are closed.' | trans }}