Adding a product form

Adding the additional API Functionality

We already have the code for the create, update and delete in the Products controller, protected with role based authentication so that only the Admin user can use this functionality. We will add another method in here though specifically to update the quantity in stock for a product. Open the API/Controllers/ProductsController.cs and add the following endpoint:

1    [InvalidateCache("api/products|")]
2    [Authorize(Roles = "Admin")]
3    [HttpPut("update-stock/{productId}")]
4    public async Task<ActionResult> UpdateStock(int productId, [FromBody]int newQuantity)
5    {
6        var productItem = await unit.Repository<Product>().GetByIdAsync(productId);
7
8        if (productItem == null)
9        {
10            return NotFound("Product not found");
11        }
12
13        productItem.QuantityInStock = newQuantity;
14
15        unit.Repository<Product>().Update(productItem);
16
17        if (await unit.Complete())
18        {
19            return Ok();
20        }
21
22        return BadRequest("Problem updating stock");
23    }
24

This will allow us to simply overwrite the QuantityInStock property with the updated quantity in stock.

Adding the Angular admin.service methods.

Add the following methods to the admin.service.ts:

1 createProduct(product: Product) {
2    return this.http.post<Product>(this.baseUrl + 'products', product);
3  }
4
5  updateProduct(product: Product) {
6    return this.http.put(this.baseUrl + 'products/' + product.id, product);
7  }
8
9  deleteProduct(id: number) {
10    return this.http.delete(this.baseUrl + 'products/' + id);
11  }
12
13  updateStock(id: number, newQuantity: number) {
14    return this.http.put(this.baseUrl + 'products/update-stock/' + id, newQuantity);
15  }

These just call the endpoints in the Products controller and are fairly self explanatory so lets move on…

Adding the product form component

We will make use of an Angular dialog component to deal with the creation of the product and make use of Reactive forms.

Create a new component for the product form in the admin feature folder:

1ng g c features/admin/product-form --skip-tests

Update the code in the product-form.component.ts to the following:

1import { Component, inject, OnInit } from '@angular/core';
2import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
3import { Product } from '../../../shared/models/product';
4import { TextInputComponent } from "../../../shared/components/text-input/text-input.component";
5import { MatButtonModule } from '@angular/material/button';
6import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
7
8@Component({
9  selector: 'app-product-form',
10  standalone: true,
11  imports: [
12    TextInputComponent,
13    MatButtonModule,
14    MatDialogModule,
15    ReactiveFormsModule,
16],
17  templateUrl: './product-form.component.html',
18  styleUrl: './product-form.component.scss'
19})
20export class ProductFormComponent implements OnInit {
21  productForm!: FormGroup;
22  data = inject(MAT_DIALOG_DATA);
23  private fb = inject(FormBuilder);
24  private dialogRef = inject(MatDialogRef<ProductFormComponent>);
25
26  ngOnInit(): void {
27    this.initializeForm();
28  }
29
30  initializeForm() {
31    this.productForm = this.fb.group({
32      name: ['', [Validators.required]],
33      description: ['', [Validators.required]],
34      price: [0, [Validators.required, Validators.min(0)]],
35      pictureUrl: ['', [Validators.required]],
36      type: ['', [Validators.required]],
37      brand: ['', [Validators.required]],
38      quantityInStock: [0, [Validators.required, Validators.min(0)]]
39    });
40  }
41
42  onSubmit(): void {
43    if (this.productForm.valid) {
44      let product: Product = this.productForm.value;
45      this.dialogRef.close({
46        product
47      })
48    }
49  }
50}
51

In this code we are using a reactive form from Angular to store the values entered by the user into the form. This will also open in a dialog so we inject the MatDialogRef on line. In the onSubmit() method we return the product from the dialog to the component that opened the dialog using the dialogRef.close() method.

Next update this components template:

1<h2 mat-dialog-title>{{data.title}}</h2>
2<mat-dialog-content class="p-3">
3  <form [formGroup]="productForm" class="p-4 w-full" (ngSubmit)="onSubmit()">
4    <app-text-input label="Product name" formControlName="name" />
5    <app-text-input label="Description" formControlName="description" />
6    <app-text-input label="Price" formControlName="price" type="number" />
7    <app-text-input label="Picture URL" formControlName="pictureUrl" />
8    <div class="flex justify-between w-full gap-3">
9      <app-text-input label="Type" formControlName="type" />
10      <app-text-input label="Brand" formControlName="brand" />
11    </div>
12
13    <div class="flex justify-end gap-3">
14      <button mat-button color="warn" type="button" mat-dialog-close>Cancel</button>
15      <button mat-raised-button color="primary" type="submit" [disabled]="productForm.invalid">
16        Submit
17      </button>
18    </div>
19
20  </form>
21</mat-dialog-content>

In this code we are making use of the re-usable text inputs we created earlier, but we will need to create more re-usable inputs for the select input (brand and type) and the text area input (description). We will create these soon.

Using the new form in the catalog component

Next we will edit the admin-catalog.component.ts so open this up in VS Code. Inject the MatDialog service:

1  private dialog = inject(MatDialog);

Add the following method in this component to open the ProductFormComponent in a dialog:

1  openCreateDialog() {
2    const dialog = this.dialog.open(ProductFormComponent, {
3      minWidth: '500px',
4      data: {
5        title: 'Create product'
6      }
7    });
8    dialog.afterClosed().subscribe({
9      next: async result => {
10        if (result) {
11          console.log(result)
12        }
13      }
14    })
15  }

In this method we use the MatDialog service to call it’s open() method and open the ProductFormComponent. We are also passing the data object and giving it a title property we then use in the product form dialog component. When the submit button is clicked in the dialog we then subscribe to the afterClosed() event so that we can retrieve the created product and then use this for the createProduct request that goes to the API which we will implement soon.

Update the admin-catalog.component.html next and add the click event to call this new method:

1  <button mat-flat-button (click)="openCreateDialog()">
2    Create new
3  </button>

You should now be able to test this which will give the following result:

Image

You can test this form and you should see the following result in the chrome dev tools console:

Image

Next we will take a look at creating re-usable components for the select input and the text-area inputs we will use in this form.