Angular 11 + DataTable + Spring Boot 2 + MongoDB

April 28, 2021 No comments Angular11 Spring Boot CRUD Example MongoDB

1. Introduction

In this tutorial, we are going to present how to create Angular application with DataTable component on the frontend and Spring Boot with MongoDB on the backend side.

2. Architecture

Let's start with presenting the architecture.

Angular11 spring boot2 mongodb datatable

In architecture we can distinguish three main layers:

  • frontend - Angular application responsible for interaction with end-user,
  • backend - Spring Boot server as a module that will communicate with the database and angular application,
  • database - MongoDB database for persisting entities.

The communication between components:

  • Spring Boot provides REST API for the Angular application,
  • Server communicates with MongoDB using Spring Data interface,
  • The Angular application sends HTTP requests and receives responses using Angular built-in HTTP client.

3. Spring Boot project

Spring Boot application is responsible for providing API and interacting with MongoDB.

3.1. Project structure

├── main
│   ├── java
│   │   └── com
│   │       └── frontbackend
│   │           └── springboot
│   │               ├── Application.java
│   │               ├── controller
│   │               │   └── EmployeesController.java
│   │               ├── model
│   │               │   ├── Employee.java
│   │               │   └── Position.java
│   │               ├── repository
│   │               │   └── EmployeeRepository.java
│   │               └── service
│   │                   └── EmployeeService.java
│   └── resources
│       └── application.properties

In Spring Boot project structure we could distinguished the following objects:

  • Application - main Spring Boot class that starts web container,
  • EmployeesController - Spring Rest Controller provides HTTP API,
  • Employee - model class for Employee,
  • Position - enum for employees positions,
  • EmployeeRespository - Spring data interface used for CRUD operations on MongoDB,
  • EmployeeService - Spring Service that performs operations on EmployeeRepository,
  • application.properties - Spring Boot configuration file.

3.2. Maven dependencies

To build the Spring Boot application, first, we need to create a new Maven project with the following dependencies:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.frontbackend.springboot</groupId>
    <artifactId>angular11-spring-boot2-mongodb-datatable</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <!-- Inherit defaults from Spring Boot -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.1.RELEASE</version>
    </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-mongodb</artifactId>
        </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>

Project dependencies:

  • spring-boot-starter-web - Spring web container,
  • spring-boot-starter-data-mongodb - Spring data for MongoDB.

The latest versions of used libraries could be found in our Maven Repository:

3.3. Model objects

In model layer we have Employee class with the following fields:

package com.frontbackend.springboot.model;

import org.springframework.data.annotation.Id;

import java.math.BigDecimal;
import java.util.Date;
import java.util.UUID;

public class Employee {

    @Id
    private UUID id;

    private String firstName;
    private String lastName;
    private Position position;
    private Date startDate;
    private BigDecimal salary;

    public UUID getId() {
        return id;
    }

    public void setId(UUID id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Position getPosition() {
        return position;
    }

    public void setPosition(Position position) {
        this.position = position;
    }

    public Date getStartDate() {
        return startDate;
    }

    public void setStartDate(Date startDate) {
        this.startDate = startDate;
    }

    public BigDecimal getSalary() {
        return salary;
    }

    public void setSalary(BigDecimal salary) {
        this.salary = salary;
    }
}

Every model object that will be persisted in MongoDB needs to have an identifier annotated with @Id. This field will uniquely identify the object.

The employee could work in the following positions:

package com.frontbackend.springboot.model;

public enum Position {

    CEO,
    SOFTWARE_ENGINEER,
    SENIOR_JAVASCRIPT_DEVELOPER,
    INTEGRATION_SPECIALIST

}

3.4. Spring Rest Controller

The EmployeesController will provide API with the following endpoints:

URL Method Action
/employees GET Get list of all Employees
/employees POST Create new Employee
/employees/delete POST Delete Employees by ids
/employees/{id} PUT Update Employee

The EmployeeController has the following structure:

package com.frontbackend.springboot.controller;

import java.util.List;
import java.util.UUID;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
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;

import com.frontbackend.springboot.model.Employee;
import com.frontbackend.springboot.service.EmployeeService;

@CrossOrigin(origins = "http://localhost:4200")
@RestController
@RequestMapping("employees")
public class EmployeesController {

    private final EmployeeService employeeService;

    public EmployeesController(EmployeeService employeeService) {
        this.employeeService = employeeService;
    }

    @GetMapping("{id}")
    public ResponseEntity<Employee> get(@PathVariable UUID id) {
        return employeeService.get(id)
                              .map(ResponseEntity::ok)
                              .orElse(ResponseEntity.notFound()
                                                    .build());
    }

    @GetMapping
    public List<Employee> list() {
        return employeeService.list();
    }

    @PostMapping
    public UUID save(@RequestBody Employee employee) {
        return employeeService.save(employee)
                              .getId();
    }

    @PutMapping("{id}")
    public Employee edit(@PathVariable UUID id, @RequestBody Employee employee) {
        employee.setId(id);
        return employeeService.save(employee);
    }

    @PostMapping("/delete")
    public void remove(@RequestBody List<UUID> ids) {
        employeeService.removeAll(ids);
    }
}

The annotations we used in this class requires explanaition:

  • @RestController - marks class with Spring stereotype intended for REST controllers,
  • @CrossOrigin - allows cross-domain requests from http://localhost:4200,
  • @RequestMapping - connects a specific endpoint with this Controller.

3.5. Service

The EmployeeService class is responsible for managing Employees. It mainly uses EmployeeRepository and adds a little business logic. The service layer separates Controller from DAO:

package com.frontbackend.springboot.service;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

import org.springframework.stereotype.Service;

import com.frontbackend.springboot.model.Employee;
import com.frontbackend.springboot.repository.EmployeeRepository;

@Service
public class EmployeeService {

    private final EmployeeRepository employeeRepository;

    public EmployeeService(EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    public Employee save(Employee employee) {
        if (employee.getId() == null) {
            employee.setId(UUID.randomUUID());
        }
        return employeeRepository.save(employee);
    }

    public void removeAll(List<UUID> ids) {
        ids.forEach(employeeRepository::deleteById);
    }

    public Optional<Employee> get(UUID uuid) {
        return employeeRepository.findById(uuid);
    }

    public List<Employee> list() {
        return employeeRepository.findAll();
    }
}

Note that we inject EmployeeRepository using a constructor that's why we don't have to add the @Autowired annotation.

3.6. Repository

The EmployeeRepository is used for interaction with MongoDB. This interface extends MongoRepository that allows to used CRUD methods like save(), delete(), findAll(), deleteById():

package com.frontbackend.springboot.repository;

import java.util.UUID;

import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;

import com.frontbackend.springboot.model.Employee;

@Repository
public interface EmployeeRepository extends MongoRepository<Employee, UUID> {
}

3.7. Configuration

The application.properties file contains entries related to MongoDB connection:

spring.data.mongodb.authentication-database=admin
spring.data.mongodb.username=mongoadmin
spring.data.mongodb.password=secret
spring.data.mongodb.database=test
spring.data.mongodb.port=27888
spring.data.mongodb.host=localhost

4. Angular project

The Angular application will be responsible for interactions with the end-user.

4.1. Project structure

Angular project has the following structure:

├── app
│   ├── app.component.html
│   ├── app.component.ts
│   ├── app.module.ts
│   ├── app-routing.module.ts
│   ├── components
│   │   ├── employee-delete
│   │   │   ├── employee-delete.component.html
│   │   │   └── employee-delete.component.ts
│   │   ├── employee-form
│   │   │   ├── employee-form.component.html
│   │   │   └── employee-form.component.ts
│   │   └── employees
│   │       ├── employees.component.html
│   │       └── employees.component.ts
│   ├── model
│   │   ├── employee.model.ts
│   │   └── position.model.ts
│   ├── services
│   │   ├── employees.service.ts
│   │   └── validation.service.ts
│   └── shared
│       ├── control-messages
│       │   └── control.messages.ts
│       └── interceptor
│           └── date.interceptor.ts
├── assets
├── environments
│   ├── environment.prod.ts
│   └── environment.ts
├── favicon.ico
├── index.html
├── main.ts
├── polyfills.ts
└── styles.css

In this structure, there is a simple division into components, services, and shared objects.

  • employees.component.ts - the main component presenting a list of employees with add, edit, remove options,
  • employee-form.component.ts - form for creating and editing employee,
  • employee-delete.component.ts - modal that presents the remove confirmation message,
  • employees.service.ts - services that communicate with REST API provided by Spring Boot application,
  • validation.service.ts - services that handle common validation errors,
  • control.messages.ts - a component that aggregates validation errors on forms,
  • date.interceptor.ts - interceptor that replaces date in string format that comes from API into JavaScript Date object,
  • environment.ts and environment.prod.ts - contains environment-specific parameters,
  • employee.model.ts and position.model.ts - are JavaScript representations of Java objects: Employee and Position.

4.2. Creating components and services

We are going to use Angular CLI to create a new project with all required components and services.

To install globally Angular CLI use the following command:

npm install -g @angular/cli

Next, we need to create a new Angular project:

ng new angular --minimal

The next step is to install all required dependencies like bootstrap or datatable:

npm install jquery --save

npm install datatables.net --save

npm install datatables.net-dt --save

npm install angular-datatables --save

npm install @types/jquery --save-dev

npm install @types/datatables.net --save-dev

npm install bootstrap --save

npm install --save @ng-bootstrap/ng-bootstrap

To create components use the following:

> ng g c components/employees
> ng g c components/employee-form
> ng g c components/employee-delete

For creating services use these commands:

> ng g s services/employees
> ng g s services/validation

In the angular.json file we need to add the following entries for bootstrap and datatable:

...
"styles": [
              ...
              "node_modules/datatables.net-dt/css/jquery.dataTables.css",
              "node_modules/bootstrap/dist/css/bootstrap.min.css",
            ],
            "scripts": [
            "node_modules/jquery/dist/jquery.js",
            "node_modules/datatables.net/js/jquery.dataTables.js",
            "node_modules/bootstrap/dist/js/bootstrap.js",
            ]
...

4.3. Environment configuration

In angular.json file by default there is an fileReplacements for production configuration:

"production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],

When we use ng build --configuration=production command Angular CLI will replace the environment file src/environments/environment.ts with src/environments/environment.prod.ts.

Without --configuration=production flag the regular file will be used environments/environment.ts that contains:

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

The baseUrl is used in EmployeesService as a root endpoint for all HTTP requests.

4.4. Angular module

In the Angular module file we need to register all created components and services:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { EmployeesComponent } from './components/employees/employees.component';
import { DataTablesModule } from 'angular-datatables';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { EmployeeFormComponent } from './components/employee-form/employee-form.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ControlMessages } from './shared/control-messages/control.messages';
import { AngularDateHttpInterceptor } from './shared/interceptor/date.interceptor';
import { EmployeeDeleteComponent } from './components/employee-delete/employee-delete.component';

@NgModule({
  declarations: [
    AppComponent,
    EmployeesComponent,
    EmployeeFormComponent,
    ControlMessages,
    EmployeeDeleteComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    DataTablesModule,
    HttpClientModule,
    NgbModule,
    FormsModule,
    ReactiveFormsModule
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AngularDateHttpInterceptor,
      multi: true
    }
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {
}

4.5. Employee Service

The EmployeesService is responsible for communication with REST API provided by Spring Boot server:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
import { Employee } from '../model/employee.model';

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

  constructor(private http: HttpClient) {
  }

  list(): Observable<Employee[]> {
    return this.http.get<Employee[]>(`${environment.baseUrl}/employees`);
  }

  deleteAll(ids: string[]): Observable<void> {
    return this.http.post<void>(`${environment.baseUrl}/employees/delete`, ids);
  }

  save(employee: Employee): Observable<void> {
    return this.http.post<void>(`${environment.baseUrl}/employees`, employee);
  }

  update(employee: Employee): Observable<void> {
    return this.http.put<void>(`${environment.baseUrl}/employees/${employee.id}`, employee);
  }
}

This service is using Angular built-in HttpClient for sending HTTP requests and receiving responses.

4.6. Date Interceptor

The AngularDateHttpInterceptor is used to convert date that comes from HTTP API in string into the JavaScript Date object:

import {
  HttpErrorResponse,
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest,
  HttpResponse
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';

@Injectable()
export class AngularDateHttpInterceptor implements HttpInterceptor {
  // Migrated from AngularJS https://raw.githubusercontent.com/Ins87/angular-date-interceptor/master/src/angular-date-interceptor.js
  iso8601 = /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(([+-]\d\d:\d\d)|Z)?$/;

  public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      tap((event: HttpEvent<any>) => {
        if (event instanceof HttpResponse) {
          const body = event.body;
          this.convertToDate(body);
        }
      }, (err: any) => {
        if (err instanceof HttpErrorResponse) {
          if (err.status === 401) {
          }
        }
      }),
    );
  }

  convertToDate(body) {
    if (body === null || body === undefined) {
      return body;
    }

    if (typeof body !== 'object') {
      return body;
    }

    for (const key of Object.keys(body)) {
      const value = body[key];
      if (this.isIso8601(value)) {
        body[key] = new Date(value);
      } else if (typeof value === 'object') {
        this.convertToDate(value);
      }
    }
  }

  isIso8601(value) {
    if (value === null || value === undefined) {
      return false;
    }

    return this.iso8601.test(value);
  }
}

To register this interceptor we need to add the following provider in the Angular main module:

providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AngularDateHttpInterceptor,
      multi: true
    }
  ],

4.7. Data model objects

In the data model layer we have two objects Employee and Position that represents Java classes Employee and enum Position:

export class Employee {
  id?: string;
  firstName?: string;
  lastName?: string;
  position?: string;
  salary?: number;
  startDate?: Date;
  checked: boolean;
}
export enum Position {
  CEO = 'CEO',
  SOFTWARE_ENGINEER = 'SOFTWARE_ENGINEER',
  SENIOR_JAVASCRIPT_DEVELOPER = 'SENIOR_JAVASCRIPT_DEVELOPER',
  INTEGRATION_SPECIALIST = 'INTEGRATION_SPECIALIST'
}

export const Position2LabelMapping: Record<Position, string> = {
  [Position.CEO]: "CEO",
  [Position.SOFTWARE_ENGINEER]: "Software engineer",
  [Position.SENIOR_JAVASCRIPT_DEVELOPER]: "Senior JavaScript Developer",
  [Position.INTEGRATION_SPECIALIST]: "Integration Specialist",
};

To convert enum values into labels we introduce the Position2LabelMapping object.

4.8. Employee List Component

The EmployeesComponent is the main component that presents a list of employees in the DataTable component:

import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Subject } from 'rxjs';
import { Employee } from '../../model/employee.model';
import { EmployeesService } from '../../services/employees.service';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { EmployeeFormComponent } from '../employee-form/employee-form.component';
import { Position2LabelMapping } from 'src/app/model/position.model';
import { DataTableDirective } from 'angular-datatables';
import { EmployeeDeleteComponent } from '../employee-delete/employee-delete.component';

@Component({
  selector: 'app-employees',
  templateUrl: './employees.component.html',
  styles: []
})
export class EmployeesComponent implements OnInit, OnDestroy {

  public Position2LabelMapping = Position2LabelMapping;

  @ViewChild(DataTableDirective, { static: false }) dtElement: DataTableDirective;

  dtTrigger: Subject<any> = new Subject<any>();
  dtOptions: any = {};
  employees: Employee[] = [];
  checkedEmployeeIds: string[] = [];
  gridCheckAll: boolean = false;
  disableEdit: boolean = true;
  disableRemove: boolean = true;

  constructor(private employeeService: EmployeesService, private modalService: NgbModal) {
  }

  ngOnInit(): void {
    this.dtOptions = {
      pagingType: 'full_numbers',
      pageLength: 5,
      processing: true,
      lengthChange: false,
    };
    this.getEmployees(true);
  }

  getEmployees(trigger: boolean): void {
    this.employeeService.list()
      .subscribe(data => {
        this.employees = data;

        if (trigger) {
          this.dtTrigger.next();
        }
      });
  }

  openForAdd(): void {
    const modalRef = this.modalService.open(EmployeeFormComponent);
    modalRef.componentInstance.employee = new Employee();
    modalRef.result.then((data) => {
      this.refreshEmployees();
    }, (reason) => {
      // on dismiss
    });
  }

  openForEdit(): void {
    const modalRef = this.modalService.open(EmployeeFormComponent);
    modalRef.componentInstance.employee = this.employees.find(e => e.id == this.checkedEmployeeIds[0]);
    modalRef.result.then((data) => {
      this.refreshEmployees();
    }, (reason) => {
      // on dismiss
    });
  }

  openDeleteConfirmation(): void {
    const modalRef = this.modalService.open(EmployeeDeleteComponent);
    modalRef.componentInstance.employees = this.employees.filter(e => this.checkedEmployeeIds.includes(e.id));

    modalRef.result.then((data) => {
      this.refreshEmployees();
    }, (reason) => {
      // on dismiss
    });
  }

  refreshEmployees(): void {
    this.getEmployees(false);
    this.checkedEmployeeIds = [];
    this.checkDisabled();
    this.rerender();
  }

  rowCheckBoxChecked(e, employeeId): void {
    if (e.currentTarget.checked) {
      this.checkedEmployeeIds.push(employeeId);
    } else {
      this.checkedEmployeeIds.splice(this.checkedEmployeeIds.indexOf(employeeId), 1);
    }

    this.checkDisabled();
  }

  gridAllRowsCheckBoxChecked(e): void {
    this.checkedEmployeeIds = [];

    if (this.gridCheckAll) {
      this.gridCheckAll = false;
      this.employees.forEach(e => e.checked = false);

    } else {
      this.gridCheckAll = true;
      this.employees.forEach(e => {
        e.checked = true;
        this.checkedEmployeeIds.push(e.id);
      });
    }

    this.checkDisabled();
  }

  checkDisabled(): void {
    this.disableEdit = this.checkedEmployeeIds.length != 1;
    this.disableRemove = this.checkedEmployeeIds.length < 1;
  }

  rerender(): void {
    this.dtElement.dtInstance.then((dtInstance: DataTables.Api) => {
      // Destroy the table first
      dtInstance.destroy();
      // Call the dtTrigger to rerender again
      this.dtTrigger.next();
    });
  }

  ngOnDestroy(): void {
    this.dtTrigger.unsubscribe();
  }
}

The HTML file for the employee component looks as follows:

<div class="row">
    <div class="col-12">
        <button class="btn btn-primary mr-1" (click)="openForAdd()">Add</button>
        <button class="btn btn-warning mr-1" (click)="openForEdit()" [disabled]="disableEdit">Edit</button>

        <button class="btn btn-danger" (click)="openDeleteConfirmation()" [disabled]="disableRemove">Delete</button>

        <table class="table table-sm row-border hover" datatable [dtOptions]="dtOptions"
               [dtTrigger]="dtTrigger">
            <thead>
            <tr>
                <th>
                    <input type="checkbox" [value]="gridCheckAll"
                           (change)="gridAllRowsCheckBoxChecked($event)"/>
                </th>
                <th>First name</th>
                <th>Last name</th>
                <th>Position</th>
                <th>Start data</th>
                <th>Salary</th>
            </tr>
            </thead>
            <tbody>
            <tr *ngFor="let e of employees">
                <td>
                    <input type="checkbox" [(ngModel)]="e.checked"
                           (change)="rowCheckBoxChecked($event, e.id)"/>
                </td>
                <td>{{e.firstName}}</td>
                <td>{{e.lastName}}</td>
                <td>{{Position2LabelMapping[e.position]}}</td>
                <td>{{e.startDate | date}}</td>
                <td>{{e.salary | currency}}</td>
            </tr>
            </tbody>
        </table>

    </div>
</div>

4.9. Employee Add/Edit Form Component

Adding and updating employee information is handled in dedicated modal:

import { Component, Input, OnInit } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { Employee } from '../../model/employee.model';
import { Position, Position2LabelMapping } from '../../model/position.model';
import { EmployeesService } from '../../services/employees.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-employee-form',
  templateUrl: './employee-form.component.html',
  styles: []
})
export class EmployeeFormComponent implements OnInit {

  @Input() employee: Employee;
  form: FormGroup;

  public Position2LabelMapping = Position2LabelMapping;
  public positions = Object.values(Position);

  constructor(private activeModal: NgbActiveModal,
              private employeeService: EmployeesService,
              private formBuilder: FormBuilder) {

    this.createForm();
  }

  private createForm() {
    this.form = this.formBuilder.group({
      firstName: [ '', Validators.required ],
      lastName: [ '', Validators.required ],
      position: [ '', Validators.required ],
      startDate: [ '', Validators.required ],
      salary: [ '', [ Validators.required, Validators.pattern('[0-9]*') ] ],
    });
  }

  validateAllFormFields(formGroup: FormGroup) {
    Object.keys(formGroup.controls).forEach(field => {
      const control = formGroup.get(field);
      control.markAsTouched({ onlySelf: true });
    });
  }

  save(): void {
    if (this.form.dirty && this.form.valid) {
      let startDate: any = this.form.value.startDate;
      let employee = this.form.value as Employee;
      employee.startDate = new Date(startDate.year, startDate.month, startDate.day);

      if (this.employee.id) {
        employee.id = this.employee.id;

        this.employeeService.update(employee).subscribe(value => {
          this.activeModal.close('Close after update');
        });
      } else {
        this.employeeService.save(employee).subscribe(value => {
          this.activeModal.close('Close after save');
        });
      }

    } else {
      this.validateAllFormFields(this.form);
    }
  }

  close(): void {
    this.activeModal.dismiss('Close click');
  }

  ngOnInit(): void {
    if (this.employee && this.employee.id) {

      const startDate = this.employee.startDate ?
        {
          day: this.employee.startDate.getDate(),
          month: this.employee.startDate.getMonth(),
          year: this.employee.startDate.getFullYear()
        } : null;

      this.form.setValue({
        firstName: this.employee.firstName,
        lastName: this.employee.lastName,
        position: this.employee.position,
        startDate: startDate,
        salary: this.employee.salary
      });
    }
  }
}

The HTML file for the employee form component looks as follows:

<form [formGroup]="form" (ngSubmit)="save()">
    <div class="modal-header">
        <h4 class="modal-title">Employee</h4>
        <button type="button" class="close" aria-label="Close" (click)="close()">
            <span aria-hidden="true">&times;</span>
        </button>
    </div>
    <div class="modal-body">
        <div class="modal-boy">
            <div class="container">
                <div class="form-group">
                    <label for="firstName">First name</label>
                    <input id="firstName"
                           type="text"
                           class="form-control"
                           autocomplete="off"
                           formControlName="firstName"/>
                    <control-messages [control]="form.get('firstName')"></control-messages>
                </div>
                <div class="form-group">
                    <label for="lastName">Last Name</label>
                    <input id="lastName"
                           type="text"
                           class="form-control"
                           autocomplete="off"
                           formControlName="lastName">
                    <control-messages [control]="form.get('lastName')"></control-messages>
                </div>
                <div class="form-group">
                    <label for="position">Position</label>
                    <select id="position"
                            class="form-control"
                            autocomplete="off"
                            formControlName="position">

                        <option value="" disabled>Choose position</option>
                        <option [value]="position" *ngFor="let position of positions">{{ Position2LabelMapping[position] }}</option>
                    </select>
                    <control-messages [control]="form.get('position')"></control-messages>
                </div>
                <div class="form-group">
                    <label for="startDate">Start date</label>
                    <div class="input-group">
                        <input id="startDate"
                               class="form-control"
                               placeholder="yyyy-mm-dd"
                               formControlName="startDate"
                               autocomplete="off"
                               name="startDate" ngbDatepicker #dp="ngbDatepicker">

                        <div class="input-group-append">
                            <button class="btn btn-outline-secondary calendar" (click)="dp.toggle()" type="button"></button>
                        </div>
                    </div>
                    <control-messages [control]="form.get('position')"></control-messages>
                </div>
                <div class="form-group">
                    <label for="salary">Salary</label>
                    <input id="salary"
                           type="text"
                           class="form-control"
                           autocomplete="off"
                           formControlName="salary">
                    <control-messages [control]="form.get('salary')"></control-messages>
                </div>
            </div>
        </div>
    </div>
    <div class="modal-footer">
        <button type="button" class="btn btn-success mr-1" (click)="save()">Save</button>
        <button type="button" class="btn btn-outline-secondary" (click)="close()">Close</button>
    </div>
</form>

If you are looking for more information about how to handle modals in Angular with Bootstrap please chec the following link: https://ng-bootstrap.github.io/#/components/modal/examples

4.10. Employee Remove Confirmation Component

To present employee remove confirmation modal we use EmployeeDeleteComponent:

import { Component, Input, OnInit } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { EmployeesService } from '../../services/employees.service';
import { Employee } from '../../model/employee.model';
import { Position2LabelMapping } from 'src/app/model/position.model';

@Component({
  selector: 'app-employee-delete',
  templateUrl: './employee-delete.component.html',
  styles: []
})
export class EmployeeDeleteComponent implements OnInit {

  @Input() employees: Employee[] = [];
  public Position2LabelMapping = Position2LabelMapping;

  constructor(private activeModal: NgbActiveModal,
              private employeeService: EmployeesService) {
  }

  ngOnInit(): void {
  }

  close(): void {
    this.activeModal.dismiss('Close click');
  }

  delete(): void {
    this.employeeService.deleteAll(this.employees.map(e => e.id)).subscribe(value => {
      this.activeModal.close('Close after remove');
    });
  }
}

The HTML file for the employee removal confirmation looks as follows:

<div class="modal-header">
    <h4 class="modal-title">Confirmation</h4>
    <button type="button" class="close" aria-label="Close" (click)="close()">
        <span aria-hidden="true">&times;</span>
    </button>
</div>
<div class="modal-body">
    <div class="modal-boy">
        <div class="container">
            <h5>Are you sure you want to remove selected employees?</h5>

            <ul>
                <li *ngFor="let e of employees">{{e.firstName}} {{e.lastName}} ({{Position2LabelMapping[e.position]}})</li>
            </ul>
        </div>
    </div>
</div>
<div class="modal-footer">
    <button type="button" class="btn btn-danger mr-1" (click)="delete()">Delete</button>
    <button type="button" class="btn btn-outline-secondary" (click)="close()">Close</button>
</div>

4.11. Validation messages

The ControlMessages component will aggregate all validation errors on forms. As an input, it takes a FormControl object.

This is the example use of ControlMessages for salary field: <control-messages [control]="form.get('salary')"></control-messages>

import { Component, Input } from '@angular/core';
import { FormControl } from '@angular/forms';
import { ValidationService } from '../../services/validation.service';

@Component({
  selector: 'control-messages',
  template: `
      <div *ngIf="errorMessage !== null" class="errorMessage">{{errorMessage}}</div>
  `,
  styles: [ '.errorMessage { color: #cb0101 }' ]
})
export class ControlMessages {
  _errorMessage: string;
  @Input() control: FormControl;

  constructor() {
  }

  get errorMessage() {
    for (let propertyName in this.control.errors) {
      if (
        this.control.errors.hasOwnProperty(propertyName) &&
        this.control.touched
      ) {
        return ValidationService.getValidatorErrorMessage(
          propertyName,
          this.control.errors[propertyName]
        );
      }
    }

    return null;
  }
}

The ValidationService contains two common validator handlers for required and pattern. This list could be easily extended with new validators:

export class ValidationService {
  static getValidatorErrorMessage(validatorName: string, validatorValue?: any) {
    let config = {
      required: 'This field is required',
      pattern: `Incorrect value, required pattern: ${validatorValue.requiredPattern}`
    };

    return config[validatorName];
  }
}

5. Start application and Testing

5.1. Starting String Boot server

To run the Spring Boot server we need to use the following command:

mvn spring-boot:run

or

java -jar target/angular11-spring-boot2-mongodb-datatable-0.0.1-SNAPSHOT.jar

To create a jar file in the target folder we must simply build our Maven project using:

mvn clean install

5.2. Starting Angular application

To start Angular application on a default port 4200 use the following:

ng serve

if there is no Angular CLI on your computer, try to install it globally:

npm install -g @angular/cli

5.1. Demo

Angular11 spring boot2 mongodb datatable

6. Conclusion

In this article, we presented Angular application with DataTable component and Spring Boot with MongoDB on the backend side.

As usual, the whole project described in this tutorial is available in our GitHub repository.

{{ message }}

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