Angular 9 + Spring Boot + PrimeNG Datatable CRUD Example
By: Nazia Shaikh
Tags: Primeng, Spring Boot, Angular, Datatables, Crud

Overview

In previous tutorials we had implemented PrimeNG datatable inplace cell editing and PrimeNG datatable row editing. In this tutorial we will be implementing PrimeNG datatable CRUD example. We will be making use of Spring Boot for the backend to expose REST API’s.

Technology Stack

We will be making use of-

Angular 9

PrimeNG

Spring Boot

Video Tutorial

Implementation

We will first be implementing the Spring Boot Backend code to expose the REST API’s for the CRUD operations. In real world application, spring boot will be storing the information using some database like MySQL. However for this example we will be storing the data in a temporary array.

The spring boot maven project will be as follows -

The pom.xml will be as follows -

<?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> <groupId>com.codeusingjava</groupId>

<artifactId>spring-boot-rest</artifactId>

<version>1.0.0</version>

<packaging>jar</packaging> <name>spring-boot-rest</name> <parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.3.0.RELEASE</version>

<relativePath /> <!-- lookup parent from repository -->

</parent> <properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>1.8</java.version>

</properties> <dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

</dependencies> <build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

</plugins>

</build>

</project>

Create the Book model class as follows -

package com.codeusingjava.model; public class Book { private String name;

private String author;

private int price; public String getName() {

return name;

} public void setName(String name) {

this.name = name;

} public String getAuthor() {

return author;

} public void setAuthor(String author) {

this.author = author;

} public int getPrice() {

return price;

} public void setPrice(int price) {

this.price = price;

} }

Create the controller class for exposing the REST API’s. We will be exposing 4 API’s.

package com.codeusingjava.controller; import java.util.ArrayList;

import java.util.List; import org.springframework.web.bind.annotation.CrossOrigin;

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; import com.codeusingjava.model.Book; @RestController

@RequestMapping("/api")

@CrossOrigin(origins = "http://localhost:4200")

public class BookController { private List<Book> books = createList(); private static List<Book> createList() {

List<Book> bookList = new ArrayList<>(); Book book1 = new Book();

book1.setName("The Godfather");

book1.setAuthor("Mario Puzo");

book1.setPrice(10); Book book2 = new Book();

book2.setName("The Fellowship of the Ring");

book2.setAuthor("J.R.R. Tolkien");

book2.setPrice(15); bookList.add(book1);

bookList.add(book2); return bookList;

} @GetMapping("/books")

public List<Book> getAllBooks() {

return books;

} @PostMapping("/books")

public Book createBook(@RequestBody Book book) {

System.out.println("Added Book - " + book.getName());

books.add(book);

return book;

} @PutMapping("/books/{name}")

public Book updateBook(@PathVariable(value = "name") String name, @RequestBody Book bookDetails) {

System.out.println("Updated Book - " + name);

for (Book book : books) {

if (book.getName().equals(name)) {

books.remove(books.indexOf(book));

books.add(bookDetails);

break;

}

}

return bookDetails;

} @DeleteMapping("/books/{name}")

public Book deleteBook(@PathVariable(value = "name") String name) {

System.out.println("Deleted Book - " + name);

Book deletedBook = null;

for (Book book : books) {

if (book.getName().equals(name)) {

books.remove(book);

deletedBook = book;

break;

}

}

return deletedBook;

}

}

Finally create the spring boot bootstrap class for starting the application -

package com.codeusingjava; import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication

public class BooksApplication { public static void main(String[] args) {

SpringApplication.run(BooksApplication.class, args);

}

}

Develop Angular Application

In a previous tutorial we had implemented PrimeNG row editing example. We will be modifying this example perform CRUD operations using the PrimeNG datatable. Download the example.

Run the following command to build the project- npm install

Start the project as follows- ng serve

If we now go to localhost:4200/books.

We can see the list of books populated in the PrimeNG datatable. This books data is currently hardcoded and being fetched from the books.json file in the assets folder.

We will first be implementing the GET functionality to get the books list from the spring boot backend to populate the datatable.

Modify the book.service.ts to make a GET call to the spring boot backend.

import { Injectable } from '@angular/core';

import { HttpClient } from '@angular/common/http'; export interface Book {

name;

price;

author;

} @Injectable({

providedIn: 'root'

})

export class BookService { constructor(private http: HttpClient) {} getBooks() {

return this.http.get<Book[]>('http://localhost:8080/api/books');

}

}

Above GET call returns observable of book array which we need to subscribe to in the book component file.

import { Component, OnInit } from '@angular/core';

import { BookService, Book } from '../service/book.service'; @Component({

selector: 'app-book-data',

templateUrl: './book-data.component.html',

styleUrls: ['./book-data.component.css']

})

export class BookDataComponent implements OnInit { books: Book[]; constructor(private bookService: BookService) { } ngOnInit() {

this.bookService.getBooks().

subscribe(books => this.books = books);

} onRowEditInit(book: Book) {

console.log('Row edit initialized');

} onRowEditSave(book: Book) {

console.log('Row edit saved');

} onRowEditCancel(book: Book, index: number) {

console.log('Row edit cancelled');

} }

Start the spring boot application and go to localhost:4200/books we will be getting the list of books from the spring boot backend.

Next we will be implementing the update book functionality. So we have previously already added the edit icons in the datatable and implemented the row edit functionality. Now on for the successful edit save we will want to make a call the spring boot backend to update the book data. Also if the edit is cancelled we will want to revert the edit made, so we will be cloning the book list before the init is initialized to store the original book list and use it if the edit is cancelled.

Modify the book.service.ts to make a PUT call to the spring boot backend with the updated book data.

import { Injectable } from '@angular/core';

import { HttpClient } from '@angular/common/http'; export interface Book {

name;

price;

author;

} @Injectable({

providedIn: 'root'

})

export class BookService { constructor(private http: HttpClient) {} getBooks() {

return this.http.get<Book[]>('http://localhost:8080/api/books');

} public updateBook(book) {

return this.http.put<Book>("http://localhost:8080/api/books" + "/"+ book.name,book);

}

}

We will be making this update call when the edit is to be saved successfully i.e in onRowEditSave method. Also we the edit is to reverted we will need the original data to be displayed again. So we will be

import { Component, OnInit } from '@angular/core';

import { BookService, Book } from '../service/book.service'; @Component({

selector: 'app-book-data',

templateUrl: './book-data.component.html',

styleUrls: ['./book-data.component.css']

})

export class BookDataComponent implements OnInit { books: Book[]; constructor(private bookService: BookService) { } ngOnInit() {

this.bookService.getBooks().

subscribe(books => this.books = books);

} clonedBooks: { [s: string]: Book; } = {};

onRowEditInit(book: Book) {

console.log('Row edit initialized');

this.clonedBooks[book.name] = { ...book };

} onRowEditSave(book: Book) {

console.log('Row edit saved');

this.bookService.updateBook(book)

.subscribe( data => {

this.ngOnInit();

alert("Book Updated successfully.");

});



} onRowEditCancel(book: Book, index: number) {

console.log('Row edit cancelled');

this.books[index] = this.clonedBooks[book.name];

delete this.clonedBooks[book.name];

} }

Go to localhost:4200 and test the edit book functionality-

Next we will be adding the delete book functionality.

Modify the book.service.ts to make a DELETE call to the spring boot backend.

import { Injectable } from '@angular/core';

import { HttpClient } from '@angular/common/http'; export interface Book {

name;

price;

author;

} @Injectable({

providedIn: 'root'

})

export class BookService { constructor(private http: HttpClient) {} getBooks() {

return this.http.get<Book[]>('http://localhost:8080/api/books');

} public updateBook(book) {

return this.http.put<Book>("http://localhost:8080/api/books" + "/"+ book.name,book);

}



public deleteBook(book) {

return this.http.delete<Book>("http://localhost:8080/api/books" + "/"+ book.name);

}

}

In the book component file add the delete function which should be called when we want to delete a book from the table-

import { Component, OnInit } from '@angular/core';

import { BookService, Book } from '../service/book.service'; @Component({

selector: 'app-book-data',

templateUrl: './book-data.component.html',

styleUrls: ['./book-data.component.css']

})

export class BookDataComponent implements OnInit { books: Book[]; constructor(private bookService: BookService) { } ngOnInit() {

this.bookService.getBooks().

subscribe(books => this.books = books);

} clonedBooks: { [s: string]: Book; } = {};

onRowEditInit(book: Book) {

console.log('Row edit initialized');

this.clonedBooks[book.name] = { ...book };

} onRowEditSave(book: Book) {

console.log('Row edit saved');

this.bookService.updateBook(book)

.subscribe( data => {

this.ngOnInit();

alert("Book Updated successfully.");

});



} onRowEditCancel(book: Book, index: number) {

console.log('Row edit cancelled');

this.books[index] = this.clonedBooks[book.name];

delete this.clonedBooks[book.name];

} deleteBook(book: Book) {

console.log('Book Deleted');



this.bookService.deleteBook(book)

.subscribe( data => {

this.ngOnInit();

alert("Book Deleted successfully.");

});



} }

Finally in the book html page we will be adding the delete icon on the click of which the deletBook function will be called.

<p-table [value]="books" dataKey="name" editMode="row">

<ng-template pTemplate="header">

<tr>

<th>Name</th>

<th>Author</th>

<th>Price</th>

</tr>

</ng-template>

<ng-template pTemplate="body" let-rowData let-editing="editing" let-ri="rowIndex">

<tr [pEditableRow]="rowData">

<td>

{{rowData.name}}



</td>

<td>

<p-cellEditor>

<ng-template pTemplate="input">

<input pInputText type="text" [(ngModel)]="rowData.author">

</ng-template>

<ng-template pTemplate="output">

{{rowData.author}}

</ng-template>

</p-cellEditor>

</td>

<td>

<p-cellEditor>

<ng-template pTemplate="input">

<input pInputText type="text" [(ngModel)]="rowData.price">

</ng-template>

<ng-template pTemplate="output">

{{rowData.price}}

</ng-template>

</p-cellEditor>

</td>

<td style="text-align:center">

<p-button class="btnAdd" icon="pi pi-trash" class="ui-button-info" style="margin-right: .5em"

(onClick)="deleteBook(rowData)"></p-button>

<button *ngIf="!editing" pButton type="button" pInitEditableRow icon="pi pi-pencil"

class="ui-button-info" (click)="onRowEditInit(rowData)"></button>

<button *ngIf="editing" pButton type="button" pSaveEditableRow icon="pi pi-check"

class="ui-button-success" style="margin-right: .5em" (click)="onRowEditSave(rowData)"></button>

<button *ngIf="editing" pButton type="button" pCancelEditableRow icon="pi pi-times"

class="ui-button-danger" (click)="onRowEditCancel(rowData, ri)"></button>

</td>

</tr>

</ng-template>

</p-table>

If we now go to localhost:4200/books we will see the delete icon, click of which will delete the book.

We will now be implementing the add book functionality.

Modify the book.service.ts to make a POST call to the spring boot backend.

import { Injectable } from '@angular/core';

import { HttpClient } from '@angular/common/http'; export interface Book {

name;

price;

author;

} @Injectable({

providedIn: 'root'

})

export class BookService { constructor(private http: HttpClient) {} getBooks() {

return this.http.get<Book[]>('http://localhost:8080/api/books');

} public updateBook(book) {

return this.http.put<Book>("http://localhost:8080/api/books" + "/"+ book.name,book);

}



public deleteBook(book) {

return this.http.delete<Book>("http://localhost:8080/api/books" + "/"+ book.name);

}



public createBook(book) {

return this.http.post<Book>("http://localhost:8080/api/books", book);

}

}

We will be adding an Add Button in the html page for adding a new book. For this we will need to get the book information from the user. We will get this information using angular form which we will be showing in a dialog box.

We will first need to import the required modules in the app.module.ts file-

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

import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module';

import { AppComponent } from './app.component';

import { BookDataComponent } from './book-data/book-data.component';

import {TableModule} from 'primeng/table';

import { FormsModule } from '@angular/forms';

import { HttpClientModule } from '@angular/common/http';

import {ButtonModule} from 'primeng/button';

import {DialogModule} from 'primeng/dialog';

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({

declarations: [

AppComponent,

BookDataComponent

],

imports: [

BrowserModule,

AppRoutingModule,

TableModule,

FormsModule,

HttpClientModule,

ButtonModule,

DialogModule,

BrowserAnimationsModule

],

providers: [],

bootstrap: [AppComponent]

})

export class AppModule { }

Next modify the book html as follows-

<p-table [value]="books" dataKey="name" editMode="row">

<ng-template pTemplate="caption">

List of Books

<p-button class="btnAdd" label="ADD" (onClick)="onBookAdd()"></p-button>

</ng-template>

<ng-template pTemplate="header">

<tr>

<th>Name</th>

<th>Author</th>

<th>Price</th>

</tr>

</ng-template>

<ng-template pTemplate="body" let-rowData let-editing="editing" let-ri="rowIndex">

<tr [pEditableRow]="rowData">

<td>

{{rowData.name}}



</td>

<td>

<p-cellEditor>

<ng-template pTemplate="input">

<input pInputText type="text" [(ngModel)]="rowData.author">

</ng-template>

<ng-template pTemplate="output">

{{rowData.author}}

</ng-template>

</p-cellEditor>

</td>

<td>

<p-cellEditor>

<ng-template pTemplate="input">

<input pInputText type="text" [(ngModel)]="rowData.price">

</ng-template>

<ng-template pTemplate="output">

{{rowData.price}}

</ng-template>

</p-cellEditor>

</td>

<td style="text-align:center">

<p-button class="btnAdd" icon="pi pi-trash" class="ui-button-info" style="margin-right: .5em" (onClick)="deleteBook(rowData)"></p-button> <button *ngIf="!editing" pButton type="button" pInitEditableRow icon="pi pi-pencil"

class="ui-button-info" (click)="onRowEditInit(rowData)"></button>

<button *ngIf="editing" pButton type="button" pSaveEditableRow icon="pi pi-check"

class="ui-button-success" style="margin-right: .5em" (click)="onRowEditSave(rowData)"></button>

<button *ngIf="editing" pButton type="button" pCancelEditableRow icon="pi pi-times"

class="ui-button-danger" (click)="onRowEditCancel(rowData, ri)"></button>



</td>

</tr>

</ng-template>



</p-table> <p-dialog header="Book Details" [(visible)]="displayDialog"

[responsive]="true" showEffect="fade" [modal]="true">

<form #BookForm="ngForm" novalidate>

<div class="ui-grid ui-grid-responsive ui-fluid" *ngIf="bookForDialog">

<div class="ui-g ui-g-12 ui-g-nopad">

<div class="ui-g-12 ui-md-3 ui-label">

<label for="fname">Book Name</label>

</div>

<div class="ui-g-12 ui-md-9">

<input pInputText id="fname" name="fname" required

[(ngModel)]="bookForDialog.name"/>

</div>

</div>

<div class="ui-g ui-g-12 ui-g-nopad">

<div class="ui-g-12 ui-md-3 ui-label">

<label for="lname">Author</label>

</div>

<div class="ui-g-12 ui-md-9">

<input pInputText id="lname" name="lname" required

[(ngModel)]="bookForDialog.author"/>

</div>

</div>

<div class="ui-g ui-g-12 ui-g-nopad">

<div class="ui-g-12 ui-md-3 ui-label">

<label for="prof">Price</label>

</div>

<div class="ui-g-12 ui-md-9">

<input pInputText id="prof" name="prof" required

[(ngModel)]="bookForDialog.price"/>

</div>

</div>



</div>

</form>

<p-footer>

<div class="ui-dialog-buttonpane ui-helper-clearfix">

<button type="submit" pButton icon="fa-check" (click)="saveBook()"

label="Save" [disabled]="!BookForm.form.valid"></button>

</div>

</p-footer>

</p-dialog>

Next in the book component file we will be making the following changes-

onBookAdd function to display the dialog box when the user clicks on add book button

saveBook function to save the book data by calling the service

import { Component, OnInit } from '@angular/core';

import { BookService, Book } from '../service/book.service'; @Component({

selector: 'app-book-data',

templateUrl: './book-data.component.html',

styleUrls: ['./book-data.component.css']

})

export class BookDataComponent implements OnInit { books: Book[];

displayDialog: boolean;

bookForDialog: Book; constructor(private bookService: BookService) { } ngOnInit() {



this.bookService.getBooks().

subscribe(books => this.books = books);

} deleteBook(book: Book) {

console.log('Book Deleted');



this.bookService.deleteBook(book)

.subscribe( data => {

this.ngOnInit();

alert("Book Deleted successfully.");

});



}

//https://stackoverflow.com/questions/59498128/what-is-the-code-s-string-boolean-in-the-angular-code-block

clonedBooks: { [s: string]: Book; } = {};

onRowEditInit(book: Book) {

console.log('Row edit initialized');

this.clonedBooks[book.name] = { ...book };

} onRowEditSave(book: Book) {

console.log('Row edit saved');

this.bookService.updateBook(book)

.subscribe( data => {

this.ngOnInit();

alert("Book Updated successfully.");

});



} onRowEditCancel(book: Book, index: number) {

console.log('Row edit cancelled');

this.books[index] = this.clonedBooks[book.name];

delete this.clonedBooks[book.name];

} onBookAdd(){

this.bookForDialog = {

name: null, price: null, author: null

};

this.displayDialog = true;

} saveBook(){

console.log('Book Saved');

this.bookService.createBook(this.bookForDialog)

.subscribe( data => {

this.ngOnInit();

alert("Book Created successfully.");

});



this.displayDialog = false;

} }

We are done with the changes. Now when the user clicks on the add button a form will be displayed. When the user clicks on the save button the book will be added.