11

I have been fiddling around with reactive forms and valueChanges subscription in Angular 2. I don;t quite get why certain form of subscribing seem not to be allowed.

this.form.get('name').valueChanges /* <- doesn't work */
  .do(changes => {
    console.log('name has changed:', changes)
    });
  .subscribe();

this.form.get('title').valueChanges.subscribe( /* <- does work */
  changes => console.log('title has changed:', changes)
);

This plunker reproduces the problem (open DevTools console to see the error):

ZoneAwareError {stack: "Error: Uncaught (in promise): TypeError: Cannot se…g.com/zone.js@0.7.5/dist/zone.js:349:25) []", message: "Uncaught (in promise): TypeError: Cannot set prope…ore.umd.js:8486:93)↵ at Array.forEach (native)", originalStack: "Error: Uncaught (in promise): TypeError: Cannot se…ps://unpkg.com/zone.js@0.7.5/dist/zone.js:349:25)", zoneAwareStack: "Error: Uncaught (in promise): TypeError: Cannot se…g.com/zone.js@0.7.5/dist/zone.js:349:25) []", name: "Error"…}

Is the first pattern (with do) not illegal indeed?

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
user776686
  • 7,933
  • 14
  • 71
  • 124
  • Why would `do` be illegal? You don't need to, you can pass the callback to `subscribe(...)` instead. The Plunker doesn't run for me (don't know why, have this since a while and therefore can't investigate) – Günter Zöchbauer Jan 16 '17 at 12:17
  • That's exactly what puzzles me: why would it be illegal? The Plunkr doesn't always like me either - I am updating my post with error from the DevTools. – user776686 Jan 16 '17 at 12:26
  • Your Plunker did miss the import for the `do` operator. See also http://stackoverflow.com/questions/34515173/angular-2-http-get-with-typescript-error-http-get-map-is-not-a-function-in/34515276#34515276 – Günter Zöchbauer Jan 16 '17 at 12:27
  • Sort of a good point, adding `import 'rxjs/add/operator/do';` did not help though. – user776686 Jan 16 '17 at 12:31
  • Hard to tell, I don't know how to debug with Plunker not working. I'm not using TypeScript on my machine. Maybe someone else has an idea. – Günter Zöchbauer Jan 16 '17 at 12:33

4 Answers4

6

one of the way is:

debounceTime will make subscription after given milliseconds. If not required,ignore.

distinctUntilChanged will force subscription only on actual value change.

Update for Angular 8+

     this.form.controls['form_control_name']
                .valueChanges
                .pipe(
                    .debounceTime(MilliSeconds)
                    .distinctUntilChanged()
                )
                .subscribe({
                    next: (value) => {
                        //use value here
                    }
                });
AbdulRehman
  • 946
  • 9
  • 16
Nabin Kumar Khatiwada
  • 1,546
  • 18
  • 19
1

This made your plunker working:

  1. add this import statement in app.ts
  import 'rxjs/add/operator/do';
  1. Remove semicolon after .do statement

.do(changes => { console.log('name has changed:', changes) })

Complete changed app.ts

import {Component, NgModule, OnInit} from '@angular/core'
import {BrowserModule } from '@angular/platform-browser'
import { FormGroup, FormBuilder, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/operator/do';

@Component({
  selector: 'my-app',
  template: `
    <form novalidate [formGroup]="form">
      <div>
        <select type="text" class="form-control" name="title" formControlName="title">
          <option *ngFor="let title of titles" value="{{title}}">{{title}}</option>
        </select>
      </div>
      <div>
        <input type="text" class="form-control" name="name" formControlName="name">
      </div>
      <fieldset formGroupName="address">
        <legend>Address</legend>
        <input type="text" class="form-control" name="street" formControlName="street">
        <input type="text" class="form-control" name="city" formControlName="city">
      </fieldset>
    </form>
  `,
})
export class App implements OnInit {
  private form: FormGroup;
  private titles: string[] =  ['Mr', 'Mrs', 'Ms'];

  constructor(private fb: FormBuilder){

  }
  ngOnInit() {
    this.form = this.fb.group({
      title: 'Mr',
      name: '',
      address: this.fb.group({
        street: '',
        city: ''
      })
    });

    this.form.get('name').valueChanges
      .do(changes => {
        console.log('name has changed:', changes)
        })
      .subscribe();

    this.form.get('title').valueChanges.subscribe(
      changes => console.log('title has changed:', changes)
    );

    this.form.get('address').valueChanges.subscribe(
      changes => console.log('address has changed:', changes)
    );  
  }
}

@NgModule({
  imports: [ BrowserModule, FormsModule, ReactiveFormsModule ],
  declarations: [ App ],
  bootstrap: [ App ]
})
export class AppModule {}

Another solution approach would be to use the "ngModelChange" event

Change this in your template:

 <input type="text" class="form-control" (ngModelChange)="doNameChange($event)" name="name" formControlName="name">

In your component you handle then your change event:

doNameChange(event: any){
   alert("change")
}
Karl
  • 3,099
  • 3
  • 22
  • 24
  • Thanks, but as you see, I do know a working way to handle the changes. The question is rather about why an legit pattern does not seem to work. – user776686 Jan 16 '17 at 12:40
  • why do you want to use "do" ? If you move the functionality from "do" to the subscribe block it is working – Karl Jan 16 '17 at 13:16
  • 2
    @user776686, your pattern is not wrong the syntax is. – vivekmore Apr 05 '18 at 14:46
  • 1
    I second it, you have a semicolon in the middle of your observable chain between do() and subscribe(). This is a code syntax error. – dudewad Apr 05 '18 at 21:33
1

You can subscribe to the whole form and pick and choose what to "take" like so... Also, don't forget to keep track of subscriptions and unsubscribe using lifecycle hooks. Also, wait until the view has loaded to subscribe to changes. This pattern works great.

private DEBOUNCE_TIME_FORM_INPUT = 250

private ngUnsubscribe: Subscription = new Subscription();

constructor(private fb: FormBuilder) {} 

ngOnInit(): void {
  this.initForm();
}

ngAfterViewInit(): void {
  this.subscribeToFilters();
}

ngOnDestroy(): void { // Don't forget to close possible memory leak
  this.ngUnsubscribe.unsubscribe();
} 

private initForm(): void {
  this.form = this.fb.group({
    name: ['', []],  
  });
}

private subscribeToFilters(): void {
  this.ngUnsubscribe.add(
    this.form.valueChanges
      .pipe(debounceTime(this.DEBOUNCE_TIME_FORM_INPUT)) // Debounce time optional
      .subscribe((form): void => {
        const { name } = form
        console.log('Name', name);
    }))
  )
} 

get name(): AbstractControl { return this.form.get(['name']); }
0

I was also having problems with this.

I wanted to do debounceTime in my case, so it didn't solve my needs.

I actually forgot to subscribe!

But I was able to get value changes to activate this way too:

this.form.get('name').valueChanges.forEach(
    (value) => {console.log(value);} )

Sourced from here in the Angular docs: hero-detail-component.ts

JGFMK
  • 8,425
  • 4
  • 58
  • 92