Implement the CRUD Operations in Angular

After creating all the necessary endpoints in my Spring Boot backend application to accept the main CRUD (Create, Read, Update, Delete) operations, it’s time to go to the frontend.

In this article, I will configure an Angular project to perform the CRUD operations.

I won’t go in the order. I will start by the Read operation as it’s the easiest one. And I will finish with the Update operation where I can edit an existing entity.

As said, I’ve already created in a previous article the backend with all the necessary endpoints. I will focus only on requesting those endpoints.

If you want a more detailed explanation, watch this video.

All the code of the article is available in the following repository.

Read

The application is a vehicle management system. So the Read endpoint is GET /vehicles to obtain all the vehicles.

Let start by creating a Vehicle class to map all the fields.

export class Vehicle {
  constructor(
  public id: number | null,
  public brand: string,
  public model: string,
  public year: number,
  public color: string,
  ) {}
}

I will request all the vehicles from the root component, and call the display component to display each vehicle individually.

Let’s start by creating the display component:

ng generate component vehicle-display

This component has as input parameters the information of a single vehicle. Let’s implement the TypeScript part.

import { Component, Input, Output, EventEmitter } from '@angular/core';
import {MatButtonModule} from '@angular/material/button';
import {MatCardModule} from '@angular/material/card';

import {Vehicle} from './../vehicle';

@Component({
  selector: 'app-vehicle-display',
  templateUrl: './vehicle-display.component.html',
  styleUrls: ['./vehicle-display.component.css'],
  standalone: true,
  imports: [MatCardModule, MatButtonModule],
})
export class VehicleDisplayComponent {

  @Input() vehicle: Vehicle = new Vehicle(0, "", "", 0, "");
}

Some points about this class:

  • I’ve used the Material components to generate the UI;
  • As I’ve used Material, I must declare it as standalone and import it the app.module.ts;
  • And finally, I only have the input parameter of a Vehicle to display.

Let’s go now to the HTML class:

<p>
  <mat-card class="vehicle-card">
    <mat-card-header>
      <mat-card-title>{{vehicle.brand}} {{vehicle.model}}</mat-card-title>
      <mat-card-subtitle>{{vehicle.year}} {{vehicle.color}}</mat-card-subtitle>
    </mat-card-header>
  </mat-card>
</p>

I just display the fields using a Material Card. Nothing complicated.

Finally, in the app.component, I must request my backend when loading the component, and call the display component when rendering.

import { Component } from "@angular/core";
import { HttpClient } from '@angular/common/http';

import {Vehicle} from './vehicle';

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"],
})
export class AppComponent {

  vehicles: Vehicle[] = [];

  constructor(private http: HttpClient) {}

  ngOnInit(): void {
    this.http.get<Vehicle[]>(
      "http://localhost:8080/vehicles"
    ).subscribe(data => this.vehicles = data);
  }
}

And in the HTML, display the vehicle-display component in a for loop.

<div class="main">
  <h2>My vehicles list</h2>
  <app-vehicle-display *ngFor="let vehicle of vehicles" [vehicle]="vehicle" />
</div>

Create

Let’s move to the Create operations.

When talking about the Create operation, I need my Angular project to upload content to the backend. This means that I need to add a form to my application.

Let’s create a dedicated component for that:

ng generate component vehicle-input

I will display this component in the root component, before displaying the list of vehicles:

<div class="main">
  <h1>Add a new vehicle</h1>

  <app-vehicle-input
    (newDataEvent)="appendData($event)"/>

  <h2>My vehicles list</h2>
  <app-vehicle-display *ngFor="let vehicle of vehicles" [vehicle]="vehicle" />
</div>

I’ve already created an Output Event in the component, because after submitting the form, I want the new data to be available in the list.

So, I must implement the method appendData which receives the submitted vehicle and adds it to the existing vehicles list.

  appendData(newVehicle: any): void {
    this.vehicles.push(newVehicle);
  }

And now, let’s create the input form:

<form #vehicleForm="ngForm" class="vehicle-form" (ngSubmit)="onSubmit()">

  <mat-form-field class="vehicle-full-width">
    <mat-label>Brand</mat-label>
    <input name="brand" ngModel required matInput>
  </mat-form-field>

  <mat-form-field class="vehicle-full-width">
    <mat-label>Model</mat-label>
    <input name="model" ngModel required matInput>
  </mat-form-field>

  <mat-form-field class="vehicle-full-width">
    <mat-label>Year</mat-label>
    <input name="year" ngModel required matInput>
  </mat-form-field>

  <mat-form-field class="vehicle-full-width">
    <mat-label>Color</mat-label>
    <mat-select name="color" required ngModel>
      <mat-option value="White">White</mat-option>
      <mat-option value="Black">Black</mat-option>
      <mat-option value="Gray">Gray</mat-option>
      <mat-option value="Red">Red</mat-option>
      <mat-option value="Blue">Blue</mat-option>
      <mat-option value="Other">Other</mat-option>
    </mat-select>
  </mat-form-field>

  <button type="submit" mat-raised-button color="primary">Submit</button>
</form>

Two important points with the previous form:

  1. Each input has the tag ngModel. This HTML tag links the field with a value in the form object. The form object will be present in the TypeScript file and it’s named vehicleForm. This variable is first declared in the form definition (at line 1).
  2. I’ve linked the ngSubmit event of the form to a method in the component named onSubmit. But the ngSubmit event will only be triggered if the button has the type submit.

Let’s finally see the TypeScript file:

import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { NgForm, FormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button';
import {MatCardModule} from '@angular/material/card';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {MatDividerModule} from '@angular/material/divider';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatIconModule} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {MatRadioModule} from '@angular/material/radio';
import {MatSelectModule} from '@angular/material/select';

import {Vehicle} from './../vehicle';

@Component({
  selector: 'app-vehicle-input',
  templateUrl: './vehicle-input.component.html',
  styleUrls: ['./vehicle-input.component.css'],
  standalone: true,
  imports: [MatFormFieldModule,
            MatInputModule,
            MatSelectModule,
            MatButtonModule,
            MatDividerModule,
            MatIconModule,
            FormsModule,
            MatCardModule,
            MatCheckboxModule,
            MatRadioModule],
})
export class VehicleInputComponent {

  @ViewChild('vehicleForm') vehicleForm!: NgForm;

  @Output() newDataEvent = new EventEmitter();

  constructor(private http: HttpClient) {}

  onSubmit(): void {
    this.http.post<Vehicle>(
      "http://localhost:8080/vehicles",
      this.vehicleForm.value
    ).subscribe(data => {
      this.newDataEvent.emit(data);
      });
  }


}

Let’s analyze the TypeScript file:

  1. As seen in the HTML file, I have a variable object vehicleForm which contains the value of each individual inputs;
  2. I have an output event emitter as seen in the root component;
  3. And in the onSubmit event, I call the backend to push the created vehicle. To push the vehicle, I use the HttpClient library. And when I get the result, in the subscribe method, I call the EventEmitter.

Delete

For the deletion, the backend needs to know which vehicle to delete. This means that the frontend needs to parametrize the request.

The endpoint is DELETE /vehicles/{id}. I must use the DELETE HTTP verb and the ID of the vehicle in the URL.

I will add a button in the display card created in with the Read operation. As the card has the Id, it will be easy build the URL.

<p>
  <mat-card class="vehicle-card">
    <mat-card-header>
      <mat-card-title>{{vehicle.brand}} {{vehicle.model}}</mat-card-title>
      <mat-card-subtitle>{{vehicle.year}} {{vehicle.color}}</mat-card-subtitle>
    </mat-card-header>
    <mat-card-actions align="end">
      <button mat-button (click)="removeItemEvent.emit(vehicle.id)">Remove</button>
    </mat-card-actions>
  </mat-card>
</p>

I don’t want to handle the click on the Delete button directly in the display-input component. As I have the list of all the vehicles in the root component, I want to handle the delete item there. This way I can remove the deleted vehicle from the existing list.

The usage of the display-input component in the root component must be adapted with the new Output event.

<div class="main">
  <h1>Add a new vehicle</h1>

  <app-vehicle-input
    (newDataEvent)="appendData($event)"/>

  <h2>My vehicles list</h2>
  <app-vehicle-display *ngFor="let vehicle of vehicles" (removeItemEvent)="removeItem($event)" [vehicle]="vehicle" />
</div>

And not the method in the root component:

  removeItem(vehicleId: number): void {
    this.http.delete(
      "http://localhost:8080/vehicles/" + vehicleId,
    ).subscribe(data => this.vehicles = this.vehicles.filter((vehicle: Vehicle) => vehicle.id != vehicleId));
  }

Update

And the last operation is the Update. I left the Update at last because I need to first read content to be updated, and I must know how to push content to the backend. This means that I will use concepts from the Read operation and from the Create operation.

How to implement the update?

I will add another button in the display card, next to the delete button. This way, I have access to the values of the Vehicle I want to delete.

When clicking on this new button, the vehicle-input component will be transformed into a form. But this time, I want a control over each field.

Let’s start creating a wrapper with inside the vehicle-input and the new vehicle-edit component.

ng generate component vehicle-edit
ng generate component vehicle-wrapper

And now the new form:

<form class="vehicle-form" (ngSubmit)="onSubmit()">

  <mat-form-field class="vehicle-full-width">
    <mat-label>Brand</mat-label>
    <input name="brand" [(ngModel)]="vehicle.brand" required matInput>
  </mat-form-field>

  <mat-form-field class="vehicle-full-width">
    <mat-label>Model</mat-label>
    <input name="model" [(ngModel)]="vehicle.model" required matInput>
  </mat-form-field>

  <mat-form-field class="vehicle-full-width">
    <mat-label>Year</mat-label>
    <input name="year" [(ngModel)]="vehicle.year" required matInput>
  </mat-form-field>

  <mat-form-field class="vehicle-full-width">
    <mat-label>Color</mat-label>
    <mat-select name="color" required [(ngModel)]="vehicle.color">
      <mat-option value="White">White</mat-option>
      <mat-option value="Black">Black</mat-option>
      <mat-option value="Gray">Gray</mat-option>
      <mat-option value="Red">Red</mat-option>
      <mat-option value="Blue">Blue</mat-option>
      <mat-option value="Other">Other</mat-option>
    </mat-select>
  </mat-form-field>

  <button type="submit" mat-raised-button color="primary">Save</button>
</form>

This time, I don’t have an NgModel object. Instead, each field is linked with a parameter of the Vehicle object.

Let’s see the TypeScript file:

import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {FormsModule } from '@angular/forms';
import {MatButtonModule} from '@angular/material/button';
import {MatCardModule} from '@angular/material/card';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {MatDividerModule} from '@angular/material/divider';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatIconModule} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {MatRadioModule} from '@angular/material/radio';
import {MatSelectModule} from '@angular/material/select';

import {Vehicle} from './../vehicle';

@Component({
  selector: 'app-vehicle-edit',
  templateUrl: './vehicle-edit.component.html',
  styleUrls: ['./vehicle-edit.component.css'],
  standalone: true,
  imports: [MatFormFieldModule,
            MatInputModule,
            MatSelectModule,
            MatButtonModule,
            MatDividerModule,
            MatIconModule,
            FormsModule,
            MatCardModule,
            MatCheckboxModule,
            MatRadioModule],
})
export class VehicleEditComponent {

   @Input() vehicle: Vehicle = new Vehicle(0, "", "", 0, "");

    @Output() editDataEvent = new EventEmitter();

    constructor(private http: HttpClient) {}

    onSubmit(): void {
      this.http.put<Vehicle>(
        "http://localhost:8080/vehicles",
        this.vehicle
      ).subscribe(data => {
        this.editDataEvent.emit(data);
        });
    }
}

In the TypeScript file, I have the Vehicle object where all the fields of the form are linked, and the onSubmit method to update the vehicle in the backend.

Let’s add the edit button in the vehicle-display component.

<p>
  <mat-card class="vehicle-card">
    <mat-card-header>
      <mat-card-title>{{vehicle.brand}} {{vehicle.model}}</mat-card-title>
      <mat-card-subtitle>{{vehicle.year}} {{vehicle.color}}</mat-card-subtitle>
    </mat-card-header>
    <mat-card-actions align="end">
      <button mat-button (click)="editItemEvent.emit(vehicle.id)">Edit</button>
      <button mat-button (click)="removeItemEvent.emit(vehicle.id)">Remove</button>
    </mat-card-actions>
  </mat-card>
</p>

And now the vehicle-wrapper only switches from vehicle-display to vehicle-edit depending on a flag.

<p>
  <app-vehicle-edit *ngIf="editable == true"
                    (editDataEvent)="handleSaveEdition($event)"
                    [vehicle]="vehicle"/>

  <app-vehicle-display *ngIf="editable == false"
                     (removeItemEvent)="removeItemEvent.emit($event)"
                     (editItemEvent)="handleEditClick()"
                     [vehicle]="vehicle"/>
</p>

Finally, the handleSaveEdition and handleEditClick methods will update the value of the editable flag.

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';

import { VehicleDisplayComponent } from './../vehicle-display/vehicle-display.component';
import { VehicleEditComponent } from './../vehicle-edit/vehicle-edit.component';

import {Vehicle} from './../vehicle';

@Component({
  selector: 'app-vehicle-wrapper',
  templateUrl: './vehicle-wrapper.component.html',
  styleUrls: ['./vehicle-wrapper.component.css'],
  standalone: true,
  imports: [VehicleDisplayComponent, VehicleEditComponent, CommonModule]
})
export class VehicleWrapperComponent {

  @Input() vehicle: Vehicle = new Vehicle(0, "", "", 0, "");
  @Output() removeItemEvent = new EventEmitter();
  editable: boolean = false;

  handleEditClick(): void {
    this.editable = true;
  }

  handleSaveEdition(vehicle: Vehicle): void {
    this.editable = false
    this.vehicle = vehicle;
  }


}

Conclusion

The backend part of the CRUD operations can be found in this article.

The Update operation can be done with a PUT or with a PATCH. In the current article I’ve used a PUT.

The meaning of the PUT verb is replace entirely. The meaning of the PATCH verb is replace partially.

As seen all along the article, each action has its HTTP verb. This means that the way the URL is built defines the action which will be performed behind.

I’ve also written another article using React instead of Angular to build the frontend part of the CRUD application.

My New ebook, How to Master Git With 20 Commands, is available now.

Leave a comment

A WordPress.com Website.