8

I am trying to add a custom validator to an input, but when I do so it triggers an ExpressionChangedAfterItHasBeenCheckedError error saying something changed from TRUE to FALSE.

I traced the problem down to the line below:

ngOnInit(): void {
    this.ipv4Fields.addControl('gateway', new FormControl('', [TabValidator.ipaddress()]));
}

If I remove the TabValidator.ipaddress() then the error disappears. Similarly if my validator forces return of 'null' then the error goes away. My validator is as follows:

private static _ipaddress(address: string): any {
    console.log('checking ip address: '+address+' valid: '+ip.isV4Format(address)+' is null: '+(address === null ? 'yes' : 'no'));
    if (!ip.isV4Format(address)) { return { 'wrongFormat': true }; }
    console.log('returning null');
    return null;
}
public static ipaddress(): ValidatorFn {
    return (control: AbstractControl): { [key: string]: any } => {
        console.log('validator returning: '+TabValidator._ipaddress(control.value));
        return TabValidator._ipaddress(control.value);
    };
}

From the console log it appears that the validator is returning the same value each time.

Can someone explain what is going wrong here? I can't explain what variable is changing from 'true' to 'false' since there are no booleans involved in the lines above. How can the validator return different values without appearing in my console log?

I read one SO questions about not doing things in ngOnInit but that seems like a red herring.


UPDATE: I tried:

ngOnInit(): void {
    Promise.resolve().then(() => {
    this.ipv4Fields.addControl('gateway', new FormControl('', [TabValidator.ipaddress()]));
    });
}

and also

ngOnInit(): void {
  setTimeout(() => this.ipv4Fields.addControl('gateway', new FormControl('', [TabValidator.ipaddress()])), 10);
}

but then error changes to:

ERROR Error: Cannot find control with name: 'gateway'


SOLUTION:

I traced this down to a bug - using reactive forms + validators + ngb tabs. Adding the validator to the control on a tab causes the error. Only solution is to change to template forms.

TSG
  • 4,242
  • 9
  • 61
  • 121
  • this mostly happens when something is not correct with the rendering order of your components. something in your lifecycle is out of order. there are a lot of threads on angular github issues page about this issue. review your components. verify if you don't have any `*ngIf` blocking the rendering of some component and so on...I ran into this problem several times and it was always because something was not correct with my code / components rendering order. – DAG Sep 07 '17 at 17:29
  • I don't have any ngIf or ngFor. I do apply different styles based on validity classes applied to the input. – TSG Sep 07 '17 at 17:32
  • yes, as I said, `*ngIf*` could be an issue, but there are a lot of things that cause that error and as I said, mostly because of the ordering of components rendering. There is too less code to tell you where is the issue. If you can reproduce in a plunker (I recommend nowadays https://stackblitz.com/) would be very helpful... – DAG Sep 07 '17 at 17:36
  • check this article [Everything you need to know about the `ExpressionChangedAfterItHasBeenCheckedError` error](https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4) – Max Koretskyi Sep 07 '17 at 19:36

1 Answers1

4

If you look at angular's github Issues page, you can find couple tickets related to same problem you have (as example https://github.com/angular/angular/issues/15634 , there are more, this one I just found as example). Long story short: angular doesn't like view change in some (all?) lifecycle hooks. What exactly changes in your particular case: I assume that is validity state of formField (i.e. internally in Angular's Control code, that you don't have access to), that internally triggers change detection...

If that is possible for you, move this.ipv4Fields.addControl() to constructor. If that is not possible - see suggestions in github tickets (do detectChanges, or Promise, or setTimeout, etc.)...

UPDATE:

@Component(){}
export class MyComponent implements OnInit{
    
    @Input()
    ipv4Fields: FormGroup;
    
    //inject `ChangeDetecorRef`
    constructor(protected changeDetectorRef: ChangeDetectorRef){
    }
    
    ngOnInit(){
        this.ipv4Fields.addControl('gateway', new FormControl('', [TabValidator.ipaddress()]));
        this.changeDetectorRef.detectChanges();
    }
}
Deitsch
  • 1,610
  • 14
  • 28
A. Tim
  • 3,046
  • 1
  • 13
  • 15
  • I tried to move to constructor but fields weren't created yet so it errored. I tried using promise (see question update above) but didn't work. Can you provide a small code example on what my ngOnInit should look like? – TSG Sep 07 '17 at 17:22
  • 1
    Personally, I'm so tired fighting with that error, so I each time I face that problem, I just do `this.changeDetectorRef.detectChanges();`. Each angular release they fix some of those problems, so I retest my code without that part. So `controller(protected changeDetectorRef: ChangeDetectorRef){} ngOnInit(){/*other code;*/ this.changeDetectorRef.detectChanges();}`. I had similar problem, when I need to update validator dynamically. My form was in modal. So instead of setting validator in `ngOnInit` I moved that code to `modalShow()` callback and that was enough %) – A. Tim Sep 07 '17 at 17:33
  • Ok I implemented the above (I think controller should be constructor) but still get the ExpressionChanged... error – TSG Sep 07 '17 at 17:40
  • sorry then, I don't have better solution now :( – A. Tim Sep 07 '17 at 17:45
  • I appreciate the ideas. Does that mean my problem is different from the bug you mentioned, or that mine is just a different manifestation of the same bug? (guessing) – TSG Sep 07 '17 at 17:52
  • Hard to say, there might be problem with interaction between parent-child component, as example... – A. Tim Sep 07 '17 at 18:06