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. setValue
must include all the controls.
patchValue
is 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>