Simple Change Detection on Angular Forms

The code for this article is available on my GitHub:
JeffreyBane/ALF-My-Form-Change-Detection-App (github.com)

We talked about preventing navigation when a form has uncommitted data a few articles back Prevent Navigation from Forms with Uncommitted Data – Angular Love Fest but what’s a good way to detect form changes so we know if we should prevent navigation or not? If you’re ever bored and think “I hope there’s 40 or 50 different ways to accomplish this”, then detecting form changes is a great topic for you to read up on!

However, today I’ll talk about a way I’ve done form change detection in the past and I like it because it’s proven quite effective over time, it’s fairly simple, and doesn’t require a lot of exotic things going on.

While this technique is fairly simple, there are some gotchas you need to be aware of that we’ll code for, so do read the whole thing. First thing we’ll need is, duh, a form. So, let’s make that component:

>> ng g c my-form

And let’s add our HTML to the form:

My Form HTML (my-form.component.html)

<form [formGroup]="myFormGroup">
    <div class="container">
        <div class="row">
            <div class="col-2"><label for="myName">Name</label></div>
            <div class="col-2"><input type="text" id="myName" 
             name="myName" formControlName="myName"></div>
        </div>
        <div class="row mt-2">
            <div class="col-2"><label for="active">Active</label></div>
            <div class="col-2"><input type="checkbox" id="active"
             name="active" formControlName="active"></div>
        </div>
        <div class="row mt-2">
            <div class="col-2"><label for="department">Department</label>
            </div>
            <div class="col-2"><select id="department" 
             name="department" formControlName="department">
                <option value="">-- Please Select --</option>
                <option value="1">Sales</option>
                <option value="2">IT</option>
                <option value="3">HR</option>
                </select>

            </div>
        </div>
        <div class="row mt-2">
            <div class="col-2"><label for="notes">Notes</label></div>
            <div class="col-2"><textarea id="notes" name="notes"
             formControlName="notes"></textarea></div>
        </div>
        <div class="row mt-2">
            <div class="col-2"><button (click)="saveForm()">Save</button>
            </div>
        </div>
    
    </div>

So, we’ve got a textbox, a checkbox, a dropdown and a text area just to cover some of the more common inputs. We also have a button to save the form as we’ll have some things to do in the SaveForm() method. Since the purpose of this article is to demonstrate if any changes in the form have taken place, what we really need as well is a link to navigate off the page. Then we can do some testing to see what happens when a user changes some of the form values and tries to leave the page. Let’s add that link at the very bottom of the above html:

    <div class="container">
        <div class="row mt-4">
            <div class="col-2">
                    <a href="javascript:void(0)"
                     (click)="tryToLeavePage()">Leave page</a>
            </div>
        </div>
    </div>

Now let’s put the basics in our My Form component file so we can run the app:

My Form Component (my-form.component.ts)

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms'


@Component({
  selector: 'app-my-form',
  templateUrl: './my-form.component.html',
  styleUrls: ['./my-form.component.scss']
})
export class MyFormComponent implements OnInit {

  myName: FormControl;
  active: FormControl;
  department: FormControl;
  notes: FormControl;

  myFormGroup: FormGroup;


  constructor() { }

  ngOnInit(): void {

    this.myName = new FormControl('');
    this.active = new FormControl('');
    this.department = new FormControl('');
    this.notes = new FormControl('');

    this.myFormGroup = new FormGroup({
      myName: this.myName,
      active: this.active,
      department: this.department,
      notes: this.notes
    })
  }

  saveForm(): void {
    alert('Saved!');
  }

  tryToLeavePage(): void {
    alert('Leaving page!');
  }

}

So fairly straightforward so far, we’ve created FormControls for our html elements and a FormGroup named myFormGroup. Two simple methods just fire off an alert when we do a simulated save or navigate away from the page.
Our humble little ugly form looks like this now:

So let’s wire up our change detector. And it’s really quite simple provided you code for aforementioned gotchas, which we will of course do. Let’s first add a class variable to our component file:

.......
  initialFormValue = '';
.......

And then we’ll set this value using JSON.stringify in the OnInit. Add this code in that method, right after the form group is built:

.......
    this.initialFormValue = JSON.stringify(this.myFormGroup.value);
    console.log(this.initialFormValue);

So, at very end of our Init, we’re setting initialFormValue to the JSON string representation of our form (and outputting it to the console.) In dev tools we can see what that variable looks like:

{"myName":"","active":"","department":"","notes":""}

As expected, a bunch of empty strings. Now let’s wire up some compare logic in our navigate away method. Replace tryToLeavePage() with the following:

  tryToLeavePage(): void {

    console.log('Initial Form: ' + this.initialFormValue);
    console.log('Current Form: ' + 
                 JSON.stringify(this.myFormGroup.value));

    if (this.initialFormValue === 
        JSON.stringify(this.myFormGroup.value)) {
      alert('Leaving page!');
    } else {
      alert('Form has changed, can\'t let you leave!!');
    }
  }

We’ve got 2 log statements so we can see what’s going on and then the guts of the method which essentially compares the JSON string of the initial FormGroup value to the current value of the FormGroup. If they match (i.e., nothing has changed), we simulate the user being allowed to navigate away from the page. If they don’t equal (some form value has changed), we prevent navigation.

If you click the “Leave page” link without making any changes to the form, you can see navigation is permitted. However, change any value in the form and click “Leave page” again and you’ll see the differences in the JSON strings outputted to the console. So, the very basics of this technique are working.

BONUS: One of my favorite aspects of this method is that if the user changes a value on the form, then changes it back to its previous value, it is NOT reported as a change. This is because we are literally comparing values as opposed to checking if a form control is pristine or dirty.

So, in the method I’m describing here, it’s really as simple as that – comparing 2 JSON strings. However, there’s more to handle to make this work properly and we’ll go over those steps one by one in the next section.

NOTE: If you really want a good example of preventing navigation away from a page that is much more comprehensive than the simple button we’re using here, something you can actually use in your apps to truly prevent navigation, including closing the tab / browser, see the article mentioned above: Prevent Navigation from Forms with Uncommitted Data – Angular Love Fest

Step 1: Account for form initial population

Let’s pretend we grabbed some values from a database when the form is loaded, as the user can come back and edit this form as much as they want. As opposed to writing a service, let’s just use consts for this example to represent the values that were retrieved from the database. In our Init method, right before setting initalFormValue, let’s add variables and setValue statements:

......    
    const myNameFromDb: string = 'John Doe';
    const activeFromDb: boolean = true;
    const departmentFromDb: number = 2;
    const notesFromDb: string = 'Some Notes';
   
    this.myFormGroup.controls.myName.setValue(myNameFromDb);
    this.myFormGroup.controls.active.setValue(activeFromDb);
    this.myFormGroup.controls.department.setValue(departmentFromDb);
    this.myFormGroup.controls.notes.setValue(notesFromDb);

If you now try modifying the form and then clicking to navigate away, everything still works. However, let’s pretend this is a new record and there aren’t any values in the database just yet. (Or perhaps only some form values were saved last time). In that case, we’d pull null from the database as well. So let’s set them all to null to see what happens.

    const myNameFromDb: string = null;
    const activeFromDb: boolean = null;
    const departmentFromDb: string  = null;
    const notesFromDb: string = null;

Now if you navigate away immediately, you’ll get the expected response that nothing has changed. However, go ahead and enter a value in name, then delete it. Also click the checkbox, then unclick it. Now the form should be in the same shape as when it was loaded however, we can see our comparison fails:

Initial Form: {"myName":null,"active":null,"department":null,"notes":null}
Current Form: {"myName":"","active":false,"department":"","notes":null}

So, when we modify a textbox control and then clear it out, the form value is not set back to null, but an empty string. And in the case of the textbox, it’s set to false. So even though conceptually nothing has changed on the form, our comparison indicates quite a lot has changed and prevents navigation – not what we want.

The way to handle this is we must exorcise all nulls from our controls when the data is loaded:

    this.myFormGroup.controls.myName.setValue(
         myNameFromDb === null ? '' : myNameFromDb);
    this.myFormGroup.controls.active.setValue(
         activeFromDb === null ? false : activeFromDb);
    this.myFormGroup.controls.department.setValue(
         departmentFromDb === null ? '' : departmentFromDb);
    this.myFormGroup.controls.notes.setValue(
         notesFromDb === null ? '' : notesFromDb);

NOW, load up the form, change all 4 values, then change them back. As we would expect, we’re now allowed to leave the form as nothing has really changed!

Big Sidebar:

Before we get to the next step, I do need to bring up a possible situation when saving these values to the database. While we’re not really using a database in this example and we’re not going to write any code for our save button in this example, this does need to be mentioned as it might affect your code. That depends 100% on the business rules of your app. Now at the shop I work at, once a user saves a form, if they don’t enter a value into a textbox, it gets saved to the database as an empty string, as conceptually at least, that is the value they saved. And this has never caused any issues for us. If that’s your case, you can skip to Step 2 or read on just in case you ever face this situation. Also, if you have the choice, I would highly recommend going this way.

However, your buiness rules might dictate that if the user saves a form, and hasn’t entered a value or clicked the checkbox that a null value be saved to the database. And that’s just fine. You just have a bit more coding to do in our hypothetical save routine, essentially checking if each control has been “touched” or not, which Angular conveniently exposes for us. Let’s use myName as an example to demonstrate this. Temporarily, change the code of the saveForm to this:

  saveForm(): void {

    const changed = (this.myFormGroup.controls.myName.value !==
                     JSON.parse(this.initialFormValue)['myName']);

    alert(' myName changed? ' + changed + ' myName touched? ' 
         + this.myFormGroup.controls.myName.touched);

    alert('Saved!');
  }

Now load up the form and click save. You’ll see that the control reports as not having been changed and not having been touched, exactly what we’d expect.
Now, enter some text into the name box, then delete it, restoring the myName control to blank. Now click save and you’ll see that changed still reports as false, but touched now reports as true and these are the checks you’d need to make in order to determine if a null should be written to the database:

If a control has no value AND the user hasn’t touched it, you’d record a null to your db.

As to how you’d have to apply this to other controls such as a checkbox is why this method doesn’t make a whole lot of sense to me. Using the same methodology – if a control is blank and the user hasn’t touched it then it’s null:

If a checkbox field in the database has a null value, when that value is loaded, the checkbox is going to show as “false” on the form even though the user hasn’t touched it yet. So, using the above logic, when saved, a null would get written back to the database as the checkbox is still false and it hasn’t been touched.

But what if the user wants it to be false? In this case, they’d have to click it true, so the checkbox control registered as having been touched, but then they’d have to click it again to set it to false! You can imagine how angry a user would be to have to be trained and remember to do that. So, in my opinion, if you have the choice, don’t persist nulls in the database from a form save, but if you must – you know how to check if a user has touched a control.

To continue, we’ll not be saving nulls to our db so restore saveForm() to it’s original simplicity:

  saveForm(): void {
    alert('Saved!');
  }


Step 2: Reset the initialFormValue

Let’s look at the other thing that can bite you in the butt when using this method if you don’t code for it. Load up the form again and change a few values.
Now click Save. The try to leave the page. Well come on, I saved (simulated) the form and it’s still telling me I can’t leave the page!

Now you might have a Save / Submit button that has a redirect at the end of it. However, if you have a Save where the user remains on the page, you’ll need to alter saveForm():

  saveForm(): void {
    this.initialFormValue = JSON.stringify(this.myFormGroup.value);
    alert('Saved!');
  }

Simple fix, but easy enough to miss. The idea being once the user saves the form, the initial values do need to be reset to whatever they saved so the navigation check works properly. Now make a bunch of changes, save the form, then try to navigate away. Victory! We get the behavior we expect. Just one final step which is super simple but also important.

Step 3: Only Reset initialFormValue if the Form is Valid

Our form controls haven’t had any validators up to this point and that would be unusual outside of this example in the real world. Let’s add a validator to name to finish this off:

    this.myName = new FormControl('', Validators.required);

With the validator added, we can now show that you don’t want to reset initialFormValue just any old time the user clicks save but only if the save operation was permitted, i.e., the form was valid and actually saved. So, let’s modify saveForm one final time:

  saveForm(): void {
    if (this.myFormGroup.valid) {
    this.initialFormValue = JSON.stringify(this.myFormGroup.value);
    alert('Saved!');
    } else
    { 
      alert('Form is not valid! (enter a name)');
    }
  }

And now we’re done! You can see by now changing some values in the form, we’re only permitted to leave once save is clicked AND the form is valid, as that’s the only time the initialFormValue is reset:

So hopefully you find this technique useful. There are just a few caveats to code for, but we end up with a pretty simple and fairly durable way to determine if there’s been a form change quickly AND to isolate only true changes to the form.

Read please:

All things Angular forms including form control states, like dirty, touched, and valid: Angular – Building a template-driven form

Leave a Reply

Your email address will not be published. Required fields are marked *