[angular] How to watch for form changes in Angular

In Angular, I might have a form that looks like this:

<ng-form>
    <label>First Name</label>
    <input type="text" ng-model="model.first_name">

    <label>Last Name</label>
    <input type="text" ng-model="model.last_name">
</ng-form>

Within the corresponding controller, I could easily watch for changes to the contents of that form like so:

function($scope) {

    $scope.model = {};

    $scope.$watch('model', () => {
        // Model has updated
    }, true);

}

Here is an Angular example on JSFiddle.

I'm having trouble figuring out how to accomplish the same thing in Angular. Obviously, we no longer have $scope, $rootScope. Surely there is a method by which the same thing can be accomplished?

Here is an Angular example on Plunker.

This question is related to angular

The answer is


If you are using FormBuilder, see @dfsq's answer.

If you are not using FormBuilder, there are two ways to be notified of changes.

Method 1

As discussed in the comments on the question, use an event binding on each input element. Add to your template:

<input type="text" class="form-control" required [ngModel]="model.first_name"
         (ngModelChange)="doSomething($event)">

Then in your component:

doSomething(newValue) {
  model.first_name = newValue;
  console.log(newValue)
}

The Forms page has some additional information about ngModel that is relevant here:

The ngModelChange is not an <input> element event. It is actually an event property of the NgModel directive. When Angular sees a binding target in the form [(x)], it expects the x directive to have an x input property and an xChange output property.

The other oddity is the template expression, model.name = $event. We're used to seeing an $event object coming from a DOM event. The ngModelChange property doesn't produce a DOM event; it's an Angular EventEmitter property that returns the input box value when it fires..

We almost always prefer [(ngModel)]. We might split the binding if we had to do something special in the event handling such as debounce or throttle the key strokes.

In your case, I suppose you want to do something special.

Method 2

Define a local template variable and set it to ngForm.
Use ngControl on the input elements.
Get a reference to the form's NgForm directive using @ViewChild, then subscribe to the NgForm's ControlGroup for changes:

<form #myForm="ngForm" (ngSubmit)="onSubmit()">
  ....
  <input type="text" ngControl="firstName" class="form-control" 
   required [(ngModel)]="model.first_name">
  ...
  <input type="text" ngControl="lastName" class="form-control" 
   required [(ngModel)]="model.last_name">

class MyForm {
  @ViewChild('myForm') form;
  ...
  ngAfterViewInit() {
    console.log(this.form)
    this.form.control.valueChanges
      .subscribe(values => this.doSomething(values));
  }
  doSomething(values) {
    console.log(values);
  }
}

plunker

For more information on Method 2, see Savkin's video.

See also @Thierry's answer for more information on what you can do with the valueChanges observable (such as debouncing/waiting a bit before processing changes).


For angular 5+ version. Putting version helps as angular makes lot of changes.

ngOnInit() {

 this.myForm = formBuilder.group({
      firstName: 'Thomas',
      lastName: 'Mann'
    })
this.formControlValueChanged() // Note if you are doing an edit/fetching data from an observer this must be called only after your form is properly initialized otherwise you will get error.
}

formControlValueChanged(): void {       
        this.myForm.valueChanges.subscribe(value => {
            console.log('value changed', value)
        })
}

To complete a bit more previous great answers, you need to be aware that forms leverage observables to detect and handle value changes. It's something really important and powerful. Both Mark and dfsq described this aspect in their answers.

Observables allow not only to use the subscribe method (something similar to the then method of promises in Angular 1). You can go further if needed to implement some processing chains for updated data in forms.

I mean you can specify at this level the debounce time with the debounceTime method. This allows you to wait for an amount of time before handling the change and correctly handle several inputs:

this.form.valueChanges
    .debounceTime(500)
    .subscribe(data => console.log('form changes', data));

You can also directly plug the processing you want to trigger (some asynchronous one for example) when values are updated. For example, if you want to handle a text value to filter a list based on an AJAX request, you can leverage the switchMap method:

this.textValue.valueChanges
    .debounceTime(500)
    .switchMap(data => this.httpService.getListValues(data))
    .subscribe(data => console.log('new list values', data));

You even go further by linking the returned observable directly to a property of your component:

this.list = this.textValue.valueChanges
    .debounceTime(500)
    .switchMap(data => this.httpService.getListValues(data))
    .subscribe(data => console.log('new list values', data));

and display it using the async pipe:

<ul>
  <li *ngFor="#elt of (list | async)">{{elt.name}}</li>
</ul>

Just to say that you need to think the way to handle forms differently in Angular2 (a much more powerful way ;-)).

Hope it helps you, Thierry


Expanding on Mark's suggestions...

Method 3

Implement "deep" change detection on the model. The advantages primarily involve the avoidance of incorporating user interface aspects into the component; this also catches programmatic changes made to the model. That said, it would require extra work to implement such things as debouncing as suggested by Thierry, and this will also catch your own programmatic changes, so use with caution.

export class App implements DoCheck {
  person = { first: "Sally", last: "Jones" };
  oldPerson = { ...this.person }; // ES6 shallow clone. Use lodash or something for deep cloning

  ngDoCheck() {
    // Simple shallow property comparison - use fancy recursive deep comparison for more complex needs
    for (let prop in this.person) {
      if (this.oldPerson[prop] !==  this.person[prop]) {
        console.log(`person.${prop} changed: ${this.person[prop]}`);
        this.oldPerson[prop] = this.person[prop];
      }
    }
  }

Try in Plunker


I thought about using the (ngModelChange) method, then thought about the FormBuilder method, and finally settled on a variation of Method 3. This saves decorating the template with extra attributes and automatically picks up changes to the model - reducing the possibility of forgetting something with Method 1 or 2.

Simplifying Method 3 a bit...

oldPerson = JSON.parse(JSON.stringify(this.person));

ngDoCheck(): void {
    if (JSON.stringify(this.person) !== JSON.stringify(this.oldPerson)) {
        this.doSomething();
        this.oldPerson = JSON.parse(JSON.stringify(this.person));
    }
}

You could add a timeout to only call doSomething() after x number of milliseconds to simulate debounce.

oldPerson = JSON.parse(JSON.stringify(this.person));

ngDoCheck(): void {
    if (JSON.stringify(this.person) !== JSON.stringify(this.oldPerson)) {
        if (timeOut) clearTimeout(timeOut);
        let timeOut = setTimeout(this.doSomething(), 2000);
        this.oldPerson = JSON.parse(JSON.stringify(this.person));
    }
}