18

I have created a simple example to demonstrate a weird issue I'm facing.

Stackblitz - https://stackblitz.com/edit/angular-change-detection-form-group

I have three components and here are they:

1 - app component

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

@Component({
  selector: 'my-app',
  template: `<hello [form]="form"></hello>
  <hr />
  <button (click)="changeFormValue()">Change Form Value</button>`,
  styleUrls: ['./app.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush

})
export class AppComponent implements OnInit {
  name = 'Angular';

  form: FormGroup;

  ngOnInit() {
    this.form = new FormGroup({
      name: new FormControl('ABC'),
      age: new FormControl('24')
    });
  }

  changeFormValue() {
    this.form.setValue({
      name: 'XYZ',
      age: 35
    })
  }
}

2 - hello component

import { Component, Input, OnChanges, ChangeDetectionStrategy } from '@angular/core';
import { FormGroup } from '@angular/forms';

@Component({
  selector: 'hello',
  template: `<form [formGroup]="form">
  <app-input [form]="form"></app-input>
  </form>`,
  styles: [``],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class HelloComponent implements OnChanges {
  @Input() form: FormGroup;

  ngOnChanges(changes) {
    console.log(changes)
  }
}

3 - input component

import { Component, Input, OnInit, OnChanges, ChangeDetectionStrategy } from '@angular/core';
import { FormGroup } from '@angular/forms';

@Component({
  selector: 'app-input',
  template: `Name : <input type="text" [formControl]="nameFormcontrol" /> {{nameFormcontrol.value}} <br /><br />
  Age : <input type="text" [formControl]="ageFormcontrol" /> {{ageFormcontrol.value}}`,
  styles: [``],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class InputComponent implements OnInit, OnChanges {
  @Input() form: FormGroup;
  nameFormcontrol;
  ageFormcontrol;

  ngOnInit() {
    this.nameFormcontrol = this.form.get('name');
    this.ageFormcontrol = this.form.get('age');
  }

  ngOnChanges(changes) {
    console.log(changes)
  }
}

In both hello component and input component, I have set the changedetection strategy to onpush. As you see above I am creating a form group instance in the app component and passing that to the child components. Now when I click on the button on app component to change the form value, it changes the value in the input fields but not the plain texts. It only works if I delete the on push change detection from both the child components. Even the ngOnChanges does not get called even if the formgroup values are changing.

enter image description here

Is n't it weird. How the change detection works for inputs and not for the plain texts here?

Could someone explain me this please? And what is the workaround of it without removing the onpush change detection.

Ansuman
  • 1,404
  • 4
  • 14
  • 32
  • Why are you setting the change detection strategy? Also where did you find this pattern - passing a reactive form 2 layers deep to an input and then expecting the top level parent and child to keep in sync seems out of line with Angular best practices. It might be worth reviewing what you've written to ensure you're not over complicating your problem with a "clever" solution. – Levidps Jul 08 '21 at 04:01

5 Answers5

24

I found a workaround to this problem although I am not sure if this is the ideal solution.

We can listen to the form group value changes and then trigger change detection in the input component

this.form.valueChanges.subscribe( () => {
  this.cdr.detectChanges()
});

This way it updates the label values as well along with the inputs.

enter image description here

Here is the solution:

https://stackblitz.com/edit/angular-change-detection-form-group-value-change-issue-resolved

I'm not sure if it's a bug from Angular but happy that I figured out some workaround :)

Ansuman
  • 1,404
  • 4
  • 14
  • 32
  • 2
    Your problem is that your UI needs to subscribe to changes on the FormControl. It doesn't do that, it just renders `nameFormcontrol.value` and then it calls it a day, because it only updates `onPush`, which means it doesn't check `nameFormcontrol.value` for a new value ever again. You need to transfer the value into something that the UI can subscribe to, e.g. an Observable. Or you go with manual change detection, as you did. I think it's a valid solution. Just remember to `unsbuscribe` in `ngOnDestroy` to avoid memory leaks. – Thomas Ebert Mar 23 '20 at 11:56
  • That's not a workaround. That's exactly how it's intended to work. When you change the Change Detection to OnPush, you're telling Angular that I'll let you know when I've made changes, and manually call for the update cycle. – Paul Story Aug 04 '21 at 21:43
  • It's happening to me when any of my components chain has Change Detection set to OnPush – Diego Juliao Mar 04 '23 at 01:11
5

You could do something better !, let me know if this works :

when you use reactive forms you can use a really cool method called updateValueAndValidity();

private changeControlValue(control, value: number) {
    control.setValue(value);
    control.updateValueAndValidity();
  }

You can also use this method when updating the validators added to a form, example:

this.control.setValidators([Validators.required, Validators.minLength(5)]);
control.updateValueAndValidity();

This should do the trick ! I think this is one of the best adventages of using reactive forms or form controls against ng-model.

I don't recommend to use valueChanges at your form as you are open to forget to de-subscribe and create a memory leak, even you remember it can be tedious to create this flow.

And remember, when using onPush change detection only three things are going to be detected as changes by Angular:

1- Inputs and Outputs. 2- Html events that the user can do, like (click). 3- Async events like subscriptions.

I hope i helped you !.

2

Simply subscribe to the control's valueChanges property in the template using the async pipe, avoiding the need for manually triggering the change detection and subscribing to valueChanges in the component.

<input [formControl]="control"/>
<p>{{control.valueChanges | async}}</p>
Rafi Henig
  • 5,950
  • 2
  • 16
  • 36
1

Angular only detects changes if the memory address of the variable changes. Setting the value does not change memory address, thus does not hit ngOnChanges.

Same goes with arrays. A simple push does not hit ngOnChanges, have to change memory address by = to new array.

Try this:

import { Component, Input, OnInit, OnChanges, ChangeDetectionStrategy } from '@angular/core';
import { FormGroup } from '@angular/forms';

@Component({
  selector: 'app-input',
  template: `
  <div formGroupName="form">
      Name : <input type="text" formControlName="name" /> {{form.value.name}} <br /><br />
      Age : <input type="text" formControlName="age" /> {{form.value.age}}
  </div>`,
  styles: [``],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class InputComponent implements OnInit, OnChanges {
  @Input() form: FormGroup;

  ngOnInit() {
  }

  ngOnChanges(changes) {
    console.log(changes)
  }
}
user1779362
  • 1,012
  • 1
  • 12
  • 29
  • But how it changes the input values without even triggering the change detection and why not the plain texts? I still don't get it. It seems a weird issue to me. Can you post an example on how to resolve this without removing the onpush? – Ansuman Dec 05 '18 at 21:04
  • Don't pass the actual form. Pass the form.value. – user1779362 Dec 05 '18 at 21:07
  • This is how I have written the input component. I just pass the form group instance from the parent and rest all things are being taken care inside the input component itself. My question is still the same, why it's updating the input value then? – Ansuman Dec 05 '18 at 21:21
  • You can very easily pass the form.value and then on the child component, create a blank form and then all you do is this.form.patchValue(inputVal) and all is good. – user1779362 Dec 05 '18 at 21:22
  • I understand that! but I don't want to do this because I am doing a lot of stuffs in the input component which I don't want to change. This is just an example I have created to explain the problem I am facing. Moreover I want to know why the change detection works for inputs and not for texts. – Ansuman Dec 05 '18 at 21:27
  • Thanks for this, Let me try it but how the inputs gets updated with the existing code? – Ansuman Dec 05 '18 at 21:48
  • They are input from the formControlName which is part of the formControlGroup name form. This tells angular to use that as the form control and then as the parents passes new values, these should automatically update the input and the text. – user1779362 Dec 05 '18 at 21:50
  • Same issue, text not changing? – user1779362 Dec 05 '18 at 22:27
  • No It does not change. I don't know what's going on with this. – Ansuman Dec 05 '18 at 22:28
0

During the automatic change detection (cd) run Angular would notice that the values of your FormGroup have been updated and updates the UI respectively.

By setting the change detection strategy to OnPush you deactivates the automatic change detection run by Angular. In this case only the internal values are updated, but Angular would not check the values of the UI.

Maybe this is a too superficial explanation of Angular's ChangeDetection, so I recommend this blog for a deeper look into this topic.

ngOnChanges does not trigger, because the object reference (memory address) of your FormGroup has not been changed. ngOnChanges is only triggered if you pass primitives to the @Input of your components. And this would also trigger a new change detection run by Angular.

To update the UI you can trigger the change detection manually by injecting ChangeDetectorRef in your parent component and calling detectChanges().

This could look like this:

constructor(private cd: ChangeDetectorRef) {}
...
changeFormValue() {
    this.form.setValue({
        name: 'XYZ',
        age: 35
    });
    // This will trigger the change detection and your input field are updated
    this.cd.detectChanges(); 
}

I hope this is understandable ;-)

Batajus
  • 5,831
  • 3
  • 25
  • 38
  • I have already tried that before. It does not work. https://stackblitz.com/edit/angular-change-detection-form-group How come it changes the values for the inputs then? – Ansuman Dec 05 '18 at 21:23
  • When I click on the button "Change form value". It changes the values inside the inputs but not the plain texts which are next to the inputs. Could you please take a look? – Ansuman Dec 05 '18 at 21:30
  • I tried. I clicked on the "Change Form Value" button and the input fields are showing the values "XYZ" and "35". – Batajus Dec 05 '18 at 21:31
  • Yeah, that works fine. What about the text beside the input fields. I am displaying the values for the inputs right next to the input fields which don't change. – Ansuman Dec 05 '18 at 21:33
  • I have added the screenshot for your reference. Please take a look and suggest. – Ansuman Dec 05 '18 at 21:41