Skip to content
Angular Essentials
GitHub

Reactive form for updating item

For now, we will work with the reactive form to save/update the grocery/sports.

ng g c components/item-save-update --standalone --skip-tests=true

Fill up the item-save-update component with some initial template. We are using Material Dialog to show a pop-up of item form.

<h2 mat-dialog-title>Item Update</h2>
<mat-dialog-content>
  <form>
    <mat-form-field>
      <mat-label>Name</mat-label>
      <input matInput>
    </mat-form-field>

    <mat-form-field>
      <mat-label>Image Url</mat-label>
      <input matInput>
    </mat-form-field>

    <mat-form-field>
      <mat-label>Price</mat-label>
      <input matInput>
    </mat-form-field>

    <mat-form-field>
      <mat-label>Description</mat-label>
      <textarea matInput></textarea>
    </mat-form-field>
  </form>
</mat-dialog-content>

<mat-dialog-actions align="end">
  <button mat-button mat-dialog-close>Cancel</button>
  <button mat-button (click)="onSaveOrUpdate()">Update</button>
</mat-dialog-actions>

Now we need to wire up this template with reactive forms. To get started create a form group (itemForm) that will act as a collection for multiple item form controls (name, imageUrl, price, and description).

import { Component, Input, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatDialogModule } from '@angular/material/dialog';
import { Card } from '../card/card.model';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';

@Component({
  selector: 'app-item-save-update',
  standalone: true,
  imports: [
    CommonModule,
    MatDialogModule,
    MatInputModule
  ],
  templateUrl: './item-save-update.component.html',
  styleUrls: ['./item-save-update.component.scss']
})
export class ItemSaveUpdateComponent implements OnInit {
  itemForm!: FormGroup;
}

A form group instance tracks the form state of a group of form control instances (for example, a form). Each control in a form group instance is tracked by name when creating the form group. We declared that itemForm is a collection of form control (form group). Now the task is to provide those values.

export class ItemSaveUpdateComponent implements OnInit {

  itemForm!: FormGroup;

  ngOnInit(): void {
    this.initItemForm();
  }

  initItemForm(): void {
    this.itemForm = new FormGroup({
      name: new FormControl(''),
      imageUrl: new FormControl(''),
      price: new FormControl(0),
      description: new FormControl('')
    });
  }
}

For now, our form items only have initial value. FormControl provides the capability to provide functions as validators out of the box.

this.itemForm = new FormGroup({
  name: new FormControl('', [Validators.required, Validators.minLength(5)]),
  imageUrl: new FormControl(''),
  price: new FormControl(0, [Validators.required, Validators.min(0)]),
  description: new FormControl('')
});

name FormControl takes two validators viz name is required and name must have min length of five. If these built-in validators are not sufficient, FormControl also accepts custom validator functions as well.

Before we register itemForm in the template, we need to register the reactive forms module. This module declares the reactive-form directives that you need to use reactive forms. Update the imports array

imports: [
    CommonModule,
    MatDialogModule,
    MatInputModule,
    ReactiveFormsModule
  ]

Update the template with the form control using the formGroup binding provided by FormControlDirective, which is also included in the ReactiveFormsModule.

<form [formGroup]="itemForm">

A form group tracks the status and changes for each of its controls, so if one of the controls changes, the parent control also emits a new status or value change. The model for the group is maintained by its members. After you define the model, you must update the template to reflect the model in the view.

The formControlName input provided by the FormControlName directive binds each individual input to the form control defined in FormGroup. The form controls communicate with their respective elements. They also communicate changes to the form group instance, which provides the source of truth for the model value.

<mat-form-field>
  <mat-label>Name</mat-label>
  <input matInput formControlName="name">
</mat-form-field>

<mat-form-field>
  <mat-label>Image Url</mat-label>
  <input matInput formControlName="imageUrl">
</mat-form-field>

<mat-form-field>
  <mat-label>Price</mat-label>
  <input matInput formControlName="price">
</mat-form-field>

<mat-form-field>
  <mat-label>Description</mat-label>
  <textarea matInput formControlName="description"></textarea>
</mat-form-field>

Erm..so how does this handle update? For update, we need to populate our form with the existing value that we want to update. Reactive forms offer two ways to do this viz setValue and patchValue.

setValue requires that the structure of the Formgroup/FormArray should match. Otherwise, it will throw an error. setValuemust include all the controls.

patchValueis used to only update the selected or required values. It will update only matching values in FormArray/FormControl and ignore the rest. patchValue can exclude some controls.

export class ItemSaveUpdateComponent implements OnInit {

  // we will use dialogRef later to dynamically close dialog when user clicks save
  private readonly dialogRef = inject(MatDialogRef);
  
  // injecting dialogData to access whatever data is passed when we initially open dialog
  // for example, we can send item/card information that we want to update
  // and use this information to population form
  private readonly dialogData = inject(MAT_DIALOG_DATA);

  itemForm!: FormGroup;

  ngOnInit(): void {
    this.initItemForm();
    this.patchItemForm();
  }

  patchItemForm(): void {
    if (this.dialogData) {
      this.itemForm.patchValue({
        name: this.dialogData.name,
        imageUrl: this.dialogData.imageUrl,
        price: this.dialogData.price,
        description: this.dialogData.description
      });
    }
  }

  onSaveOrUpdate(): void {
    this.dialogRef.close({ data: this.itemForm.value });
  }
}

valueChanges can be used to listen to changes on the template.

ngOnInit(): void {
  this.initItemForm();
  this.patchItemForm();
  this.itemForm.valueChanges.subscribe(data => console.log(data));
}

Let’s make some changes changes in CardComponent to facilitate opening dialog and passing initial data for update.

@Component({
  selector: 'app-card',
  standalone: true,
  templateUrl: './card.component.html',
  styleUrls: ['./card.component.scss'],
  imports: [
    CommonModule,
    MatCardModule,
    MatButtonModule,
    MatDialogModule
  ]
})
export class CardComponent {

  private readonly router = inject(Router);
  private readonly route = inject(ActivatedRoute);
  private readonly dialog = inject(MatDialog);

  @Input({ required: true }) cards: Card[] = [];

  goToItemDetails(data: Card): void {
    this.router.navigate(['card-item', data.id], { state: { data }, relativeTo: this.route }).then();
  }

  openItemDialog(data: Card): void {
    const dialogRef = this.dialog.open(ItemSaveUpdateComponent, {
      data // initial data to dialog (remember dialogData in ItemSaveUpdateComponent)
    });
    dialogRef.afterClosed().subscribe(result => {
      if (result?.data) {
        // emit update event and call service from parent to update card
      }
    });
  }
}
<mat-card-actions>
  <button mat-button color="primary" (click)="openItemDialog(card)">Update</button>
  <button mat-button color="primary" (click)="goToItemDetails(card)">Details</button>
</mat-card-actions>