0

I am trying to implement two way data binding on a date input while applying a pipe on the model:

<!-- The name variable is retrieved within the HTML from a loop expression -->
<input matInput [matDatepicker]="datePicker" placeholder="{{label}}" formControlName="{{name}}"
[ngModel]="data[name] | date"
(ngModelChange)="handleDateChange(data[name], $event)" >

Where my component looks something like:

//...
private data: any;

constructor(private cdRef:ChangeDetectorRef) { }

handleDateChange(field: any, date: any) {
    field = date; // The code is simplified, there is more handling with the date field
    this.cdRef.detectChanges();
}

Based on this post, the code above works well only thanks to this.cdRef.detectChanges(); If I remove it I get the following error: Expression has changed after it was checked

My question is more general than related specifically to the above implementation. I would like to know what are the best practices to deal with the Expression has changed after it was checked error? Is calling cdRef.detectChanges() a good idea?

Strider
  • 3,539
  • 5
  • 32
  • 60

3 Answers3

3

Did you try the direct assignment to the value on the change event?

<!-- The name variable is retrieved within the HTML from a loop expression -->
<input matInput [matDatepicker]="datePicker" placeholder="{{label}}" formControlName="{{name}}"
[ngModel]="data[name] | date"
(ngModelChange)="data[name] = $event" >

With this approach you get to use the pipe on the ngModel and you keep it simple as if it were doing the regular [(ngModel)] two way binding. No need for an additional handler.

EDIT

Another reason for this to be happening can be by trying to mix the template driven approach with the reactive forms approach. I'm assuming you're not trying to do both, correct? By seeing the formControlName attribute it might be telling me you are trying to mix both approaches and definitely you'll have this issue because of that.

The reason for this is because the ngModel will update the value whenever a change happens on the template driven approach and at the same time the reactive forms approach will also update the value on the same property. Because this happens in parallel it triggers an error when Angular runs the second check for changes in dev mode. The property update should be handled only by one of the approaches.

Check a scenario where this happens here

Hugo Noro
  • 3,145
  • 2
  • 13
  • 19
  • There is more handling with the date field in my code than just a simple assignment. But still, I tried that out and I still get the error – Strider Dec 08 '17 at 11:31
  • Just a quick question: I'm assuming the names returned in the for loop are always unique, right? – Hugo Noro Dec 08 '17 at 12:12
  • Just edited my answer with a possible cause of your problem. – Hugo Noro Dec 08 '17 at 12:21
  • This is a good remark, I totally messed up by using Reactive and Template-Driven at the same time. I understand that `Expression has changed after it was checked` error should be dealt with according to each specific situation. Can you please add that to your answer so I can accept it? thanks – Strider Dec 08 '17 at 14:56
  • Edited my answer. – Hugo Noro Dec 08 '17 at 16:40
  • Removing `formControlName` attribute worked for me, thank you! – nothingisnecessary Feb 02 '18 at 21:59
  • Nevermind. removing `formControlName` from the MatDatePicker removed the "expression has changed" error, but then model didn't bind. Turns out my problem was actually a poorly formatted default value for the date string ("2/2/2018" instead of UTC string). Using proper UTC formatted string made error go away. – nothingisnecessary Feb 02 '18 at 22:22
2
(ngModelChange)="handleDateChange(data[name], $event)"

This is not correct if you add debug lines in handleDateChange(data[name], $event) you would see date might undefined. You have to put $event as the first argument.

You dont' need to pass data[name] into handleDateChange(), data is accessible in your component. what you need do is:

xxx.component.html

(ngModelChange)="handleDateChange($event)"

xxx.component.ts:

handleDateChange(event: any) {
  // change your data
  this.data[this.name] = event;
}
Haifeng Zhang
  • 30,077
  • 19
  • 81
  • 125
  • Thank you for pointing out the importance of the order of the `$event` field. As for the `name` variable, it's retrieved from within a loop inside the HTML (I just edited the code of my question to make it more clear) – Strider Dec 07 '17 at 21:15
0

try changing this

handleDateChange(field: any, date: any) {
    field = date;
    this.cdRef.detectChanges();
}

to

<input matInput [matDatepicker]="datePicker" placeholder="{{label}}" formControlName="{{name}}"
[ngModel]="data[name] | date"
(ngModelChange)="handleDateChange(name, $event)" >

  ...


  handleDateChange(field, date) {
    this.data[field] = date;
  }

let me know if you need any explanation why the Expression has changed after it was checked happens and why this.cdRef.detectChanges(); helps

SebOlens
  • 519
  • 3
  • 15
  • An explanation would be welcome :) Unfortunately the code didn't work, I still get the `Expression has changed after it was checked` error – Strider Dec 07 '17 at 20:36