Friday, June 16, 2023

Angular 16 Spring Boot 3 Spring Data GraphQL CRUD

Angular 16 was released in April 2022, and it includes a number of new features and improvements. Some of the key features of Angular 16 include:

  • Standalone components: Angular 16 introduces the concepts of standalone components, which are components that can be used independently of any other angular code. this makes it easier to reuse components and to create more modular applications.
  • Signals:  Signals are a new way of handling reactivity in Angular. Signals provide a simpler and more efficient way to notify the framework when data changes.
  • Non-destructive hydration: Angular 16 introduces a new approach to hydration called no-destructive hydration. This approach allows Angular applications to be loaded more efficiently and to perform better in terms of performance.
  • Improved security: Angular 16 includes a number of security improvements, including support for native Trusted Types and CSP.
  • For more.





Creating  Angular Application

In this example I integration Angular, GraphQL, and Spring Boot 3.

  • Configuration Angular 
  • Components
  • Services

Angular GraphQL API 

Replace with your GraphQL API URL

MethodsURLs
POSThttp://localhost:8080/graphql

Technology

  • Visual Studio Code 
  • Node 16
  • Npm
  • Angular Cli 16
  • Angular Material 16

Project Structure






















Install Angular Material

ng add @angular/material


app.module.ts

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

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AuthorsComponent } from './components/authors/authors.component';
import { MatTableModule } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatDialogModule } from '@angular/material/dialog';

import {MatIconModule} from '@angular/material/icon';
import {MatDividerModule} from '@angular/material/divider';
import {MatButtonModule} from '@angular/material/button';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';

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

import { AuthorsService } from './services/authors.service';
import { DialogAuthorComponent } from './components/dialog/dialog-author/dialog-author.component';

@NgModule({
  declarations: [
    AppComponent,
    AuthorsComponent,
    DialogAuthorComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    MatTableModule,
    MatPaginatorModule,
    MatSortModule,
    MatIconModule,
    MatDividerModule,
    MatButtonModule,
    MatFormFieldModule,
    MatDialogModule,
    MatInputModule,
    HttpClientModule,
    FormsModule
  ],
  providers: [AuthorsService],
  bootstrap: [AppComponent]
})
export class AppModule { }


Components

authors.component.ts

import { AfterViewInit, Component, ViewChild, OnInit } from '@angular/core';
import { MatTable } from '@angular/material/table';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatDialog } from '@angular/material/dialog';
import { DialogAuthorComponent } from '../dialog/dialog-author/dialog-author.component';
import { MatTableDataSource } from '@angular/material/table';
import { Subscription } from 'rxjs';

import { AuthorsService } from '../../services/authors.service';

// TODO: Replace this with your own data model type
export interface AuthorsItem {
  id: number;
  firstName: string;
  lastName: string;
  bookRecords: BookItem[];
}

export interface BookItem {
  id: number;
  title: string;
  author: AuthorsItem;
}

@Component({
  selector: 'app-authors',
  templateUrl: './authors.component.html',
  styleUrls: ['./authors.component.scss']
})
export class AuthorsComponent implements AfterViewInit, OnInit {
  @ViewChild(MatPaginator) paginator!: MatPaginator;
  @ViewChild(MatSort) sort!: MatSort;
  @ViewChild(MatTable) table!: MatTable<AuthorsItem>;
  dataSource: any;
  private subscription: Subscription = new Subscription;

  /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
  displayedColumns = ['id', 'firstName', 'lastName', 'update', 'delete'];

  constructor(public dialog: MatDialog, private authorService: AuthorsService) { }

  ngOnInit(): void { }

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

  ngAfterViewInit(): void {
    this.getAllAuthors();
  }

  getAllAuthors(): void {

    this.subscription = this.authorService.getAllAuthors().subscribe({
      next: (response: any) => {
        // Handle successful response, if needed
        this.dataSource = new MatTableDataSource(response.data.getAllAuthors);
        this.table.dataSource = this.dataSource;
        this.dataSource.sort = this.sort;
        this.dataSource.paginator = this.paginator;
      },
      error: (error: any) => {
        console.error('Error updating author:', error);
      },    // errorHandler
    });

  }

  deleteAuthor(id: number): void {

    this.authorService.deleteAuthor(id).subscribe({
      complete: () => {
        console.log('Author deleted');
        this.getAllAuthors();

      }, // completeHandler
      error: (error: any) => {
        console.error('Error deleting author:', error);
      },    // errorHandler
    });

  }

  openDialog(enterAnimationDuration: string, exitAnimationDuration: string, mode: string, row?: any): void {
    const dialogRef = this.dialog.open(DialogAuthorComponent, {
      width: '250px',
      enterAnimationDuration,
      exitAnimationDuration,
      data: {
        mode: mode,
        id: (mode === 'create' ? null : row.id),
        firstName: (mode === 'create' ? '' : row.firstName),
        lastName: (mode === 'create' ? '' : row.lastName)
      }
    });

    dialogRef.afterClosed().subscribe(
      data => {
        if (data === 1) {
          this.getAllAuthors();
        }
      }
    );

  }

  applyFilter(event: Event) {
    const filterValue = (event.target as HTMLInputElement).value;
    this.dataSource.filter = filterValue.trim().toLowerCase();
  }

}




authors.component.scss


.mat-mdc-form-field {
  margin-bottom: 16px; /* Adjust the margin-bottom as desired */
  width: 100%; /* Adjust the width as desired */
}

.mat-mdc-form-field .mat-mdc-input-element {
  border-radius: 4px; /* Adjust the border radius as desired */
  background-color: #f1f1f1; /* Set the background color for the input field */
  padding: 8px; /* Adjust the padding as desired */
}

.mat-mdc-form-field .mat-mdc-input-element:focus {
  outline: none; /* Remove the focus outline */
  background-color: #ffffff; /* Set the background color when the input field is focused */
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); /* Add a box shadow when the input field is focused */
}


.mat-mdc-row:nth-child(even) {
  background-color: #f1f1f1; /* Set the background color for even rows */
}

.mat-mdc-row:nth-child(odd) {
  background-color: #ffffff; /* Set the background color for odd rows */
}

.mat-mdc-table .mdc-data-table__header-row {
  height: 35px;
}

.mat-mdc-table .mdc-data-table__row{
  height: 35px;
}

.mdc-fab {
  position: relative;
  display: inline-flex;
  position: relative;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: 35px;
  height: 35px;
  padding: 0;
  border: none;
  fill: currentColor;
  text-decoration: none;
  cursor: pointer;
  user-select: none;
  overflow: visible;
  transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1),opacity 15ms linear 30ms,transform 270ms 0ms cubic-bezier(0, 0, 0.2, 1);
}

.header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px;
  background-color: #f1f1f1;
}

.graphql-header {
  font-size: 24px;
  font-weight: bold;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-grow: 1;
  color: #444;
}

.button-container {
  display: flex;
  align-items: center;
  justify-content: flex-end;
}

.content-container {
  display: flex;
  align-items: center;
}

.text {
  margin-right: 8px; /* Adjust the margin as desired */
}

.table-container {
  margin: 20px;
}

@media (max-width: 600px) {
  .header {
    flex-direction: column;
    align-items: flex-start;
  }

  .button-container {
    margin-top: 10px;
  }

  .mdc-fab {
    width: 20px;
    height: 20px;
  }

  .mat-mdc-table .mdc-data-table__header-row {
    height: 20px;
  }
 
  .mat-mdc-table .mdc-data-table__row{
    height: 20px;
  }

}


authors.component.html

<div class="header">
  <div class="graphql-header">
    Author CRUD - GraphQL with Spring Boot 3
  </div>
  <div class="button-container">
    <button mat-raised-button color="primary" (click)="openDialog('0ms', '0ms', 'create')">
      <span class="content-container">
        <span class="text">Create Author</span>
        <span class="icon-container">
          <mat-icon>add</mat-icon>
        </span>
      </span>
    </button>
  </div>
</div>



<div class="table-container">
 
  <mat-form-field>
    <input matInput (keyup)="applyFilter($event)" placeholder="Filter">
  </mat-form-field>

  <table mat-table  matSort aria-label="Elements">
    <!-- Id Column -->
   <ng-container matColumnDef="id">
     <th mat-header-cell *matHeaderCellDef mat-sort-header>Id</th>
     <td mat-cell *matCellDef="let row">{{row.id}}</td>
   </ng-container>
 
  <!-- Name Column -->
   <ng-container matColumnDef="firstName">
     <th mat-header-cell *matHeaderCellDef mat-sort-header>Name</th>
     <td mat-cell *matCellDef="let row">{{row.firstName}}</td>
   </ng-container>


    <!-- lastName Column -->
    <ng-container matColumnDef="lastName">
      <th mat-header-cell *matHeaderCellDef mat-sort-header>Last name</th>
      <td mat-cell *matCellDef="let row">{{row.lastName}}</td>
    </ng-container>
 
    <ng-container matColumnDef="update">
     <th mat-header-cell *matHeaderCellDef mat-sort-header></th>
     <td mat-cell *matCellDef="let row">
       <button mat-fab color="primary" aria-label="Example icon button with a update icon" (click)="openDialog('0ms', '0ms', 'edit', row)">
         <mat-icon>edit</mat-icon>
       </button>
     </td>
   </ng-container>
    <ng-container matColumnDef="delete">
     <th mat-header-cell *matHeaderCellDef mat-sort-header></th>
     <td mat-cell *matCellDef="let row">
       <button mat-fab color="warn" aria-label="Example icon button with a delete icon" (click)="deleteAuthor(row.id)">
         <mat-icon>delete</mat-icon>
       </button>
     </td>
   </ng-container>

   <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
   <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
 </table>


 
  <mat-paginator #paginator [pageIndex]="0" [pageSize]="10"
    [pageSizeOptions]="[5, 10, 20]" aria-label="Select page">
  </mat-paginator>


</div>


dialog-author.component.ts

import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { AuthorsService } from '../../../services/authors.service';

@Component({
  selector: 'app-dialog-author',
  templateUrl: './dialog-author.component.html',
  styleUrls: ['./dialog-author.component.scss']
})
export class DialogAuthorComponent {

  firstName: string;
  lastName: string;
  id: number;
  mode: string;

  constructor(public dialogRef: MatDialogRef<DialogAuthorComponent>,
    @Inject(MAT_DIALOG_DATA) public data: any,
    private authorService: AuthorsService) {

    this.mode = data.mode;
    this.firstName = data.firstName || '';
    this.lastName = data.lastName || '';
    this.id = data.id || 0;
  }

  onCancel(): void {
    this.dialogRef.close();
  }


  updateAuthor(): void {

    this.authorService.updateAuthor(this.id, this.firstName, this.lastName).subscribe({
      next: (response: any) => {
        // Handle successful response, if needed
        console.log('Author updated:', response.data.updateAuthor);
        this.dialogRef.close(1);
      }, // completeHandler
      error: (error: any) => {
        console.error('Error updating author:', error);
        this.dialogRef.close(0);
      },    // errorHandler
    });

  }

  createAuthor(): void {

    this.authorService.createAuthor(this.firstName, this.lastName).subscribe({
      next: (response: any) => {
        // Handle successful response, if needed
        console.log('Author created:', response.data.createAuthor);
        this.dialogRef.close(1);
      }, // completeHandler
      error: (error: any) => {
        this.dialogRef.close(0);
        console.error('Error creating author:', error);
      },    // errorHandler
    });

  }

}


dialog-author.component.scss

.dialog-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 16px;
  }
 
  .dialog-title {
    display: flex;
    align-items: center;
    font-weight: bold;
  }
 
  .dialog-title mat-icon {
    margin-right: 4px;
  }
 
  .dialog-title span {
    font-size: 20px;
    font-weight: bold;
    margin: 0;
  }
 
  .close-button {
    margin-left: 8px;
  }

.mdc-dialog__title {
    font-size: 20px;
    font-weight: bold;
    margin-bottom: 16px;
}

.mdc-dialog__content {
    display: flex;
    flex-direction: column;
    margin-bottom: 16px;
}

.mdc-dialog__content mat-form-field {
    width: 100%;
    margin-bottom: 8px;
}

.mdc-dialog__actions {
    display: flex;
    justify-content: flex-end;
}

.mdc-dialog__actions button {
    margin-left: 8px;
}

/* Responsive styles */
@media screen and (max-width: 600px) {
    .dialog-header {
      flex-direction: column;
      align-items: flex-start;
    }
   
    .dialog-title {
      margin-bottom: 8px;
    }
   
    .mdc-dialog__actions {
      flex-direction: column;
    }
   
    .mdc-dialog__actions button {
      margin-top: 8px;
    }
  }


dialog-author.component.html

<div class="dialog-header">
  <div class="dialog-title">
    <mat-icon>person_add</mat-icon>
    <span>{{ mode === 'create' ? 'Create Author' : 'Edit Author' }}</span>
  </div>
  <button mat-icon-button class="close-button" (click)="onCancel()">
    <mat-icon>close</mat-icon>
  </button>
</div>

<div mat-dialog-content>
  <ng-container *ngIf="mode === 'edit'">
    <mat-form-field>
      <input matInput [(ngModel)]="id" [disabled]="true" placeholder="ID">
    </mat-form-field>
  </ng-container>
  <mat-form-field>
    <input matInput [(ngModel)]="firstName" placeholder="First Name">
  </mat-form-field>
  <mat-form-field>
    <input matInput [(ngModel)]="lastName" placeholder="Last Name">
  </mat-form-field>
</div>
<div mat-dialog-actions>
  <button mat-button (click)="onCancel()">Cancel</button>
  <button mat-button color="primary" (click)="mode === 'create' ? createAuthor() : updateAuthor()">
    {{ mode === 'create' ? 'Create' : 'Update' }}
  </button>
</div>


Services

authors.service.ts: We can find GraphQL Queries and Mutations.

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

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

  constructor(private http: HttpClient) { }

  getAllAuthors(): Observable<any> {
    const query = `
      query {
        getAllAuthors {
          id
          firstName
          lastName
          bookRecords {
            id
          }
        }
      }
    `;
    return this.http.post<any>(environment.apiUrl, { query });
  }

  getAuthorById(id: number): Observable<any> {
    const query = `
      query {
        getAuthorById(id: ${id}) {
          firstName
          lastName
          bookRecords {
            id
          }
        }
      }
    `;
    return this.http.post<any>(environment.apiUrl, { query });
  }

  createAuthor(firstName: string, lastName: string): Observable<any> {
    const mutation = `
      mutation {
        createAuthor(firstName: "${firstName}", lastName: "${lastName}") {
          id
          firstName
          lastName
        }
      }
    `;
    return this.http.post<any>(environment.apiUrl, { query: mutation });
  }

  updateAuthor(id: number, firstName: string, lastName: string): Observable<any> {
    const mutation = `
      mutation {
        updateAuthor(id: ${id}, firstName: "${firstName}", lastName: "${lastName}") {
          id
          firstName
          lastName
        }
      }
    `;
    return this.http.post<any>(environment.apiUrl, { query: mutation });
  }

  deleteAuthor(id: number): Observable<any> {
    const mutation = `
      mutation {
        deleteAuthor(id: ${id})
      }
    `;
    return this.http.post<any>(environment.apiUrl, { query: mutation });
  }

}


Environments

environment.ts

// This file can be replaced during build by using the `fileReplacements` array.
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.

export const environment = {
    production: false,
    apiUrl: "http://localhost:8080/graphql" // Replace with your GraphQL API URL
  };
 


Run & Test


Run Angular application with command: ng serve





And Create, Read, Update and Delete.


Creating a Spring Boot Application


Integration Spring boot 3, Spring Data JPA, GraphQL, HyperSQL and Java 17

Detailed description and steps for run backend project found here in this post:












Automating Deployments with CronJobs in Google Kubernetes Engine (GKE)

In the realm of container orchestration, automation plays a pivotal role in streamlining operations. Google Kubernetes Engine (GKE) offers r...