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
Methods | URLs |
---|---|
POST | http://localhost:8080/graphql |
Technology
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
Go to http://localhost:4200/
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:
Source Code
Here on GitHub: