42

I have angular reactive form in parent component and sections inside childrens component.

Inside the child component I have a checkbox - when its checked - more fields open and I want them all to be required.

I am using setValidators but I'm getting error

ParentFormComponent.html:3 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ng-valid: true'. Current value: 'ng-valid: false'. at viewDebugError (core.js:7601) at expressionChangedAfterItHasBeenCheckedError (core.js:7589) at checkBindingNoChanges (core.js:7691) at checkNoChangesNodeInline (core.js:10560) at checkNoChangesNode (core.js:10541) at debugCheckNoChangesNode (core.js:11144) at debugCheckRenderNodeFn (core.js:11098) at Object.eval [as updateRenderer] (ParentFormComponent.html:3) at Object.debugUpdateRenderer [as updateRenderer] (core.js:11087) at checkNoChangesView (core.js:10442)

ParentFormComponent.html:3 ERROR CONTEXT DebugContext_ {view: Object, nodeIndex: 2, nodeDef: Object, elDef: Object, elView: Object}

this is the line of ParentFormComponent.html:3

 <form [formGroup]="parentForm" (ngSubmit)="submitForm()">

Here is my code:

<label class="container">DB
    <input #db type="checkbox" name="db" (change)="checkValue(db.name, db.checked)"> 
    <span class="checkmark"></span>
</label>

<div *ngIf="db.checked" formGroupName="_monitorDB">
    <mat-form-field>
        <input matInput placeholder="Server name" formControlName="ServerName">
    </mat-form-field>

    <mat-form-field>
        <input matInput placeholder="DataBase name" formControlName="DbName">
    </mat-form-field>

    <mat-form-field>
        <input matInput placeholder="table name" formControlName="DB_tableName">
    </mat-form-field>

    <mat-form-field>
        <input matInput placeholder="port" formControlName="DbPort">
    </mat-form-field>

    <mat-form-field>
        <input matInput placeholder="Query" formControlName="Query">
    </mat-form-field>

    <mat-form-field>
        <input matInput placeholder="Permissions" formControlName="Premissions">
    </mat-form-field>

</div>

and in the ts file:

checkValue(name:string, event: any){
    if (event == true){
      this.test.push(name);
      if (name =="db"){
         this.childForm.get('_monitorDB').get('ServerName').setValidators([Validators.required]);
         this.childForm.get('_monitorDB').get('DbName').setValidators([Validators.required]);
         this.childForm.get('_monitorDB').get('DB_tableName').setValidators([Validators.required]);
         this.childForm.get('_monitorDB').get('DbPort').setValidators([Validators.required]);
         this.childForm.get('_monitorDB').get('Query').setValidators([Validators.required]);
         this.childForm.get('_monitorDB').get('Premissions').setValidators([Validators.required]);

      }

    }

    else{
      const index: number = this.test.indexOf(name);
      if (index !== -1) {
          this.test.splice(index, 1);
          if (name =="db"){
            this.childForm.get('_monitorDB').get('ServerName').clearValidators();
            this.childForm.get('_monitorDB').get('ServerName').updateValueAndValidity();
            this.childForm.get('_monitorDB').get('DbName').clearValidators();
            this.childForm.get('_monitorDB').get('DbName').updateValueAndValidity();
            this.childForm.get('_monitorDB').get('DB_tableName').clearValidators();
            this.childForm.get('_monitorDB').get('DB_tableName').updateValueAndValidity();
            this.childForm.get('_monitorDB').get('DbPort').clearValidators();
            this.childForm.get('_monitorDB').get('DbPort').updateValueAndValidity();
            this.childForm.get('_monitorDB').get('Query').clearValidators();
            this.childForm.get('_monitorDB').get('Query').updateValueAndValidity();
            this.childForm.get('_monitorDB').get('Premissions').clearValidators();
            this.childForm.get('_monitorDB').get('Premissions').updateValueAndValidity();
          }
      }      
    }

     this.checkboxArr.emit(this.test);
 }
Dale K
  • 25,246
  • 15
  • 42
  • 71
Livnat Menashe
  • 831
  • 2
  • 9
  • 17

8 Answers8

57

I faced the same issue and I fixed it by using AfterViewChecked and ChangeDetectorRef:

import { AfterViewChecked, ChangeDetectorRef } from '@angular/core'

export class ClassName implements AfterViewChecked {
  constructor(private readonly changeDetectorRef: ChangeDetectorRef) {}

  ngAfterViewChecked(): void {
    this.changeDetectorRef.detectChanges();
  }
}

developer033
  • 24,267
  • 8
  • 82
  • 108
Justin Joseph
  • 3,001
  • 1
  • 12
  • 16
  • 2
    works, thanks! would have been nice to have an explanation what this fix does. *ngAfterVIewChecked* is called every time Angular has finished running change detection on a component and it's children. Great intro: https://indepth.dev/posts/1058/a-gentle-introduction-into-change-detection-in-angular – ukie Jul 15 '21 at 14:36
  • Fantastic solution. Worked for me like a charm. Thanks – AllJs Dec 20 '21 at 04:57
  • 2
    This is the correct solution – Md. Mahmud Hasan Feb 03 '22 at 12:12
  • 11
    Even if this solves the problem, calling `detectChanges()` every time `ngAfterViewChecked()` lifecycle hook is called, isn't optimal. Just see how many times `ngAfterViewChecked()` is called (with a `console.log`). A better solution is calling `detectChanges()` just in the moment you need it (for the concrete problem in the main post: after the validators are set). – José Antonio Postigo Feb 15 '22 at 11:23
  • 1
    As @JoséAntonioPostigo mentioned above, `ngAfterViewChecked()` is not necessary. In my case, calling `detectChanges()` alone right after adding a control worked fine. – Rosty Jul 06 '22 at 20:36
  • this worked for me.. tks <3 – Elson Costa Jul 08 '23 at 13:25
12

Change the ChangeDetectionStrategy on the parent component to : changeDetection: ChangeDetectionStrategy.OnPush. Simple and clean.

Edit This actually not a solution, it will just hide the error. Please take a look at the github link posted in the comments.

mozpider
  • 366
  • 2
  • 12
  • Can you elabore more on that? Why change the detection strategy? – Bárbara Este Jul 30 '20 at 20:05
  • 1
    Please see more discussion here: https://github.com/angular/angular/issues/23657#issuecomment-653939615 – mozpider Jul 30 '20 at 23:07
  • 2
    According to the github thread you share, this "solution" actual don't resolve the error but just hide it and can break a lot of thing. So before using it, make sure what the ChangeDetectionStrategy.OnPush imply for you – Hayha Feb 12 '21 at 12:47
  • 1
    At the time, it let me get past that error, so I posted the solution. But I have edited my solution. You will have to debug this error. – mozpider Mar 03 '21 at 21:28
7

I am facing same issue I know it's not a way but the only solution I've found is setTimeout

ngAfterViewInit(): void {
 setTimeout(() => {
    this.form.patchValue({
    name: 'xyz',
    email: 'abc@gmail.com',
    phone: '0987654321'
   })
  }, );
}
DINESH Adhikari
  • 1,242
  • 1
  • 12
  • 11
3

I don't know if it's the right thing to do but i solved it by changing:

<div *ngIf="db.checked" formGroupName="_monitorDB">

to

<div [hidden]="!db.checked" formGroupName="_monitorDB">
Livnat Menashe
  • 831
  • 2
  • 9
  • 17
  • 1
    this is not the right practice, because your elements are available in the form, which creates a performance issue if you have large form. If my provided answer isn't helpful for you, then please let me know, I will give you an answer without rxweb approach. – Ajay Ojha Dec 23 '18 at 09:58
  • This worked for me where I tried to show or hide a part of the form when I use a checkbox to do this procedure. – Vah Run Jul 23 '19 at 20:46
1

Adding changeDetection: ChangeDetectionStrategy.OnPush should do a trick

@Component({
  selector: 'app-test-center-location-step3',
  templateUrl: './test-center-location-step3.component.html',
  styleUrls: ['./test-center-location-step3.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
San Jaisy
  • 15,327
  • 34
  • 171
  • 290
1

Track where the field is changed and mark the component for changeDetection... it could be in a function or one of Angular lifecycle hooks like AfterViewChecked

constructor(
    private _cdr: ChangeDetectorRef
) {
}

// mark for check where change is happening
this._cdr.markForCheck();
this._cdr.detectChanges();

Changing changeDetection to ChangeDetectionStrategy.OnPush will also solve the problem but it might cause other problems, you will need to manually mark the component for check.

EGN
  • 2,480
  • 4
  • 26
  • 39
1

I tend to never use setTimeout as I think it's a bad practice, but I have lately found that when creating custom components and interacting with the DOM, this is sometimes necessary.

However, when I've used it, it has been with a delay of 0ms. Just wrapping the action in a setTimeout with no delay will be enough to move the action to the end of the execution queue, which will solve numerous issues with the interaction between Angular and the DOM.

TL,DR

setTimeout(() => actionThatCausesTheError(), 0);
Javi Marzán
  • 1,121
  • 16
  • 21
  • Thanks, works for me, i have this custom form that list a bunch of questions from backend dynamically, then i need to patch the value after retrieval of the answers. – Ron Michael Jun 25 '23 at 04:18
0

try this one:

ngOnInit():void
{
   this._monitorDB.patchValue({
    ServerName: 'ServerName',
    DbName: 'DbName',
    DB_tableName: 'DB_tableName',
    DbPort: 'DbPort',
    Query: 'Query',
    Premissions:'Premissions'
   });
}