159

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.

khashashin
  • 1,058
  • 14
  • 40
tambler
  • 3,009
  • 2
  • 23
  • 26
  • instead of watching some data from your controller, I think you should fire an event (like the old ng-change) from your form. – Deblaton Jean-Philippe Jan 05 '16 at 15:37
  • btw, the reason for removing the scope is to get rid of those watchers. I don't think there is a watch hidden somewhere in angular 2 – Deblaton Jean-Philippe Jan 05 '16 at 15:38
  • 5
    If I'm understanding you correctly, you're suggesting that I add an `(ngModelChange)="onModelChange($event)"` attribute to *every* form input in order to accomplish this? – tambler Jan 05 '16 at 15:39
  • 1
    Does the form instance itself not emit some kind of change event? If so, how do you access it? – tambler Jan 05 '16 at 15:40
  • If I were sure about what to do, I would have answered instead of commenting. I don't have used angular 2.0 yet, but from what I've read, the watch has completely disappeared to have an event-based framework (instead of the deep watch performed at each digest) – Deblaton Jean-Philippe Jan 05 '16 at 15:52

6 Answers6

201

UPD. The answer and demo are updated to align with latest Angular.


You can subscribe to entire form changes due to the fact that FormGroup representing a form provides valueChanges property which is an Observerable instance:

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

In this case you would need to construct form manually using FormBuilder. Something like this:

export class App {
  constructor(private formBuilder: FormBuilder) {
    this.form = formBuilder.group({
      firstName: 'Thomas',
      lastName: 'Mann'
    })

    this.form.valueChanges.subscribe(data => {
      console.log('Form changes', data)
      this.output = data
    })
  }
}

Check out valueChanges in action in this demo: http://plnkr.co/edit/xOz5xaQyMlRzSrgtt7Wn?p=preview

dfsq
  • 191,768
  • 25
  • 236
  • 258
  • 2
    This is very close to the mark. To confirm - are you telling me that it is *not* possible to subscribe to a form's `valueChanges` event emitter *if* that form is defined solely within the template? In other words - within a component's constructor, is it not possible to get a reference to a form that was defined solely within that component's template and not with the help of FormBuilder? – tambler Jan 05 '16 at 16:37
  • this is the right answer. If you don't have a form builder you are doing template driven forms. there is probably a way to still get the form injected but if you want the Observable form.valueChanges you should definitively use formBuilder and abandon ng-model – Angular University Jan 05 '16 at 21:57
  • 2
    @tambler, you can get a reference to NgForm using `@ViewChild()`. See my updated answer. – Mark Rajcok Jan 05 '16 at 22:03
  • @jhadesdev, see the comment above I just made. You can get a reference to the form and still use ngModel. See my answer and plunker. – Mark Rajcok Jan 05 '16 at 22:04
  • Im going to take that back, your answer might be exactly what OP is looking for, +1. I guess if the user is trying to migrate an existing form by using ng-model but still use the form observable its a valid use case after all. But if the user is buiding a form from scratch and was mixing up FormBuilder with template driven forms then this is the best answer for him – Angular University Jan 05 '16 at 22:12
  • When using @Components({**template:`your_inline_form`**}) this.form.valueChanges.subscribe(..) won't work. – Mandark Feb 25 '16 at 11:37
  • 1
    You don't need to unsubscribe on destroy? – Bazinga Oct 20 '16 at 14:37
  • What about unsubscribe in the onDestroy? There is not memory leak potential here? @FunStuff do you any idea? – galvan Aug 11 '17 at 10:59
  • 1
    @galvan I don't think there can be a leak. The form is part of the component and it will be properly disposed on destroy with all its fields and event listeners. – dfsq Aug 11 '17 at 11:05
  • Should it be inside constructor or ngOnInit? – Pritesh Acharya Nov 14 '17 at 18:02
  • @PriteshAcharya Doesn't really matter. – dfsq Nov 14 '17 at 19:57
107

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).

Community
  • 1
  • 1
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
61

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

Mark
  • 90,562
  • 7
  • 108
  • 148
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • Property 'valueChanges' does not exist on type 'string' – Toolkit Dec 14 '16 at 17:29
  • I have stuck, in TS file, where the check form change method should be placed? If we store it in ngAfterViewInit() {}, It's seem that the this.form.valueChanges always call if we needed to implement some processing chains for updated data in forms. – Tài Nguyễn Dec 25 '17 at 04:10
2

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)
        })
}
Krishnadas PC
  • 5,981
  • 2
  • 53
  • 54
1

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

N8allan
  • 2,138
  • 19
  • 32
0

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));
    }
}
Ulfius
  • 619
  • 7
  • 14