A very common use case for web forms is to alert the user if they attempt to navigate away from the page with unsaved changes. In technical terms, when a form's state has changed from its original, pristine, state, we refer to the form as being "dirty". For us Angular developers, the NgModel directive provides a mechanism for tracking control states. In today's tutorial, we'll update our form from the Using Material Font Icons in your Angular 11 Projects article to check for form dirtiness.

About the NgModel Directive

When you use the NgModel Directive, it creates a FormControl instance from a domain model and binds it to a form control element. That allows us to tether the value of a form control to a variable. For instance, if we had a name variable:

export class PersonComponent {
  public name: string = '';
...

...we could assign it to the NgModel using either one-way, or, as in this case, two-way binding, using both [()] square brackets and parentheses:

<input [(ngModel)]="name" #ctrl="ngModel" required>

One of the benefits of using the NgModel directive on a control, besides binding, is that it tracks the state of that control. Behind the scenes, Angular sets special CSS classes on the control element to reflect the state, as shown in the following list:

  • The control has been visited - ng-touched/ng-untouched
  • The control's value has changed - ng-dirty/ng-pristine
  • The control's value is valid - ng-valid/ng-invalid

You can reference these CSS classes in your stylesheets to alter the styling of your controls based on their status.

Using the NgModel Directive in a Form

In the last tutorial, we built a survey form that contained several Material Icons pertaining to sentiment:

angular material demo page

The above screenshot comes from the project demo on Codesandbox.io.

Here's the form markup for that project:

<p>How would you rate Angular Material Icons?</p>

<mat-icon aria-label="Example home icon">sentiment_very_dissatisfied</mat-icon>
  
<mat-icon aria-label="Example home icon">sentiment_dissatisfied</mat-icon>
  
<mat-icon aria-label="Example home icon">sentiment_neutral</mat-icon>
  
<mat-icon aria-label="Example home icon">sentiment_satisfied</mat-icon>
  
<mat-icon aria-label="Example home icon">sentiment_very_satisfied</mat-icon>

Now let's update the form so that we can track changes. Take a look at the updated template:

<form #f="ngForm" name="satisfactionForm">
  <p>How would you rate Angular Material Icons?</p>

  <mat-button-toggle-group name="toggleGroup" #group="matButtonToggleGroup" [(ngModel)]="satisfactionRating" aria-label="Satisfaction Rating">
    <mat-button-toggle [value]="satisfactionRating.very_dissatisfied" (click)="onClick(group.value)">
      <mat-icon aria-label="Very Dissatisfied">sentiment_very_dissatisfied</mat-icon>
    </mat-button-toggle>
    <mat-button-toggle [value]="satisfactionRatings.dissatisfied" (click)="onClick(group.value)">
      <mat-icon aria-label="Dissatisfied">sentiment_dissatisfied</mat-icon>
    </mat-button-toggle>
    <mat-button-toggle [value]="satisfactionRatings.neutral" (click)="onClick(group.value)">
      <mat-icon aria-label="Neutral">sentiment_neutral</mat-icon>
    </mat-button-toggle>
    <mat-button-toggle [value]="satisfactionRatings.satisfied" (click)="onClick(group.value)">
      <mat-icon aria-label="Satisfied">sentiment_satisfied</mat-icon>
    </mat-button-toggle>
    <mat-button-toggle [value]="satisfactionRatings.very_satisfied" (click)="onClick(group.value)">
      <mat-icon aria-label="Very Satisfied">sentiment_very_satisfied</mat-icon>
    </mat-button-toggle>
  </mat-button-toggle-group>

  <p>Any other comments?</p>
  <textarea name="txtComments" matInput [(ngModel)]="comments"></textarea>

  <p>
    <button mat-raised-button color="primary" (click)="onSubmit(f)">Submit</button>
  </p>
</form>

In the above markup, note that:

  • All form controls are enclosed within <form> tags.
  • The sentiment icons are now part of a mat-button-toggle-group, which is bound to the ngModel.
  • There are two new form controls: the txtComments textarea and a submit button.

Clicking the submit button invokes the onSubmit() method, passing in the "f" ngForm reference. You can see the onSubmit() method at the bottom of the AppComponent, shown below:

import { Component } from "@angular/core";
import { NgForm } from "@angular/forms";

enum SatisfactionRatings {
  none_selected,
  very_dissatisfied,
  dissatisfied,
  neutral,
  satisfied,
  very_satisfied
}

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  public title = "Keeping Track of Form Changes In Angular";
  public satisfactionRatings = SatisfactionRatings;
  public satisfactionRating = SatisfactionRatings.none_selected;
  public comments: string = '';
  
  public onClick(value: SatisfactionRatings) {
    console.log(value);
  }

  public onSubmit(f: NgForm) {
    console.log(f.value);  // { first: '', last: '' }
    console.log(f.dirty);  // false
  }
}

In the onSubmit(), we can now access the NgForm attributes, including its state. The form value consists of an object that includes each form control name and its value, while the dirty property is a boolean where a value of true signifies that the form contents are no longer pristine.

Necessary Imports

In order to use NgForms, you'll need to add FormsModule to your import AppModule imports. Moreover, your MaterialModule will need MatButtonToggleModule, MatInputModule, and MatButtonModule to support our additional controls.

Conclusion

Here's the demo of today's project on Codesandbox.io. Be sure to open the Console so that you can see the onSubmit() output.

ngmodel

The toggleGroup value of "2" refers to our SatisfactionRatings enum. It's zero-based, 2 stands for dissatisfied. (I probably should of rated them more highly!)

The main issue to tracking from changes via NgForm is that it isn't very smart. Once you change something, the form is considered to be dirty. So changing form values back to what they were originally will not put the form back into a pristine state. For that, you need to implement something a bit more robust.


Rob Gravelle resides in Ottawa, Canada, and has been an IT Guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.






This article was originally published on February 24, 2021