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.
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
Copy
├── 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:
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 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:
Copy
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:
Copy
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:
Copy
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:
Copy
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()
:
Copy
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:
Copy
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:
Copy
├── 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:
Copy
npm install -g @angular/cli
Next, we need to create a new Angular project:
Copy
ng new angular --minimal
The next step is to install all required dependencies like bootstrap or datatable :
Copy
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:
Copy
> ng g c components/employees
> ng g c components/employee-form
> ng g c components/employee-delete
For creating services use these commands:
Copy
> 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:
Copy
...
"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:
Copy
"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:
Copy
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:
Copy
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:
Copy
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:
Copy
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:
Copy
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
:
Copy
export class Employee {
id?: string;
firstName?: string;
lastName?: string;
position?: string;
salary?: number;
startDate?: Date;
checked: boolean;
}
Copy
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:
Copy
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:
Copy
<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>
Adding and updating employee information is handled in dedicated modal:
Copy
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:
Copy
<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">×</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
:
Copy
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:
Copy
<div class="modal-header">
<h4 class="modal-title">Confirmation</h4>
<button type="button" class="close" aria-label="Close" (click)="close()">
<span aria-hidden="true">×</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>
Copy
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:
Copy
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:
Copy
mvn spring-boot:run
or
Copy
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:
Copy
mvn clean install
5.2. Starting Angular application
To start Angular application on a default port 4200 use the following:
Copy
ng serve
if there is no Angular CLI on your computer, try to install it globally:
Copy
npm install -g @angular/cli
5.1. Demo
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 .
{{ 'Comments (%count%)' | trans {count:count} }}
{{ 'Comments are closed.' | trans }}