pop pop - 17 days ago 9
TypeScript Question

Angular2: View updates when data changes, but not with ngif

I am building a simple calendar with Angular2. All works fine (and as intended) with this code:

app.component.ts

import { Component, OnInit } from '@angular/core';
import { Calendar } from '@npm/calendar';

export class CalendarWeek {
days: Array<any>;
data?: string; // reserved for further use
}

@Component({
selector: 'my-app',
templateUrl: 'app/app.component.html',
styleUrls: ['app/app.component.css']
})

export class AppComponent implements OnInit {

title = 'Calendar';
weeks:CalendarWeek[];

months:string[] = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];

weekDays:string[] = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday'
];

monthString:string;
month:number;
year:number;

prevMonth:Boolean;
nextMonth:Boolean;
thisMonth:Boolean;
showMonth:Boolean;

getWeeks(): void {

let cal = new Calendar(1); // start with Monday
let mdc = cal.monthDates(this.year, this.month);

this.weeks = [];
this.monthString = this.months[this.month];
this.prevMonth = this.nextMonth = this.thisMonth = false;

for (let i=0; i<mdc.length; i++) {
this.weeks.push({ days: mdc[i] });
}
}

getThisMonth(): void {
this.month = new Date().getMonth();
this.year = new Date().getFullYear();
this.getWeeks();
}

getPreviousMonth(): void {
this.getMonth(-1);
}

getNextMonth(): void {
this.getMonth(+1);
}

getMonth(direction): void {
if ( this.month == 11 && direction > 0 ) {
this.year++;
this.month = 0;

} else if (this.month == 0 && direction < 0) {
this.year--;
this.month = 11;

} else {
this.month = this.month + direction;
}

this.getWeeks();
}

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


app.component.html

<h1>{{title}}</h1>

<div class="period">
<a (click)="getPreviousMonth()" class="monthPager">&lt;</a>
<a (click)="getThisMonth()" class="monthPager">now</a>
<a (click)="getNextMonth()" class="monthPager">&gt;</a>
<span class="now">{{monthString}}, {{year}}</span>
</div>

<div class="week">
<div *ngFor="let day of weekDays" class="week-day">
<span>{{day}}</span>
</div>
</div>

<div *ngFor="let week of weeks" class="week">
<div *ngFor="let day of week.days" class="day">
<span class="day-date">{{day | date:'d'}}</span>
</div>
</div>


As I said, everything works fine and I can page through the months back and forth.
npm/calendar
module gives a simple array of array of date objects for every week of the month, from Monday to Sunday. The thing is that the first and the last week can also include dates from the previous or the next month and so I want to check for these cases and display the name of the month, once. So I would have
October 31, November 1, 2, 3, [...], 30, December 1, 2, 3, 4
.

To do this check I added one line to the app.component.html, so it looks like this:

<div *ngFor="let week of weeks" class="week">
<div *ngFor="let day of week.days" class="day">
<span *ngIf="checkMonth(day)" class="day-month">{{day | date:'MMM'}}</span>
<span class="day-date">{{day | date:'d'}}</span>
</div>
</div>


and this method to the app.component.ts:

checkMonth(day): Boolean {

let mo = day.getMonth();
this.showMonth = false;

if ( mo < this.month && !this.prevMonth) {
this.prevMonth = true;
this.showMonth = true;
}

if ( mo > this.month && !this.nextMonth) {
this.nextMonth = true;
this.showMonth = true;
}

if ( mo == this.month && !this.thisMonth) {
this.thisMonth = true;
this.showMonth = true;
}

return this.showMonth;

}


And now I have, for once, an error

Subscriber.ts:238 EXCEPTION: Error in app/app.component.html:17:14 caused by: Expression has changed after it was checked. Previous value: 'true'. Current value: 'false'.


and I can't page through months anymore. The clicks are still being evaluated and a new array is being generated, but the view will not update. Oh, and strangely enough, also checkMonth seems to evaluate the dates as intended, it still doesn't display the names of the months - I just got pure dates, no month names.

What could cause the problem here?
Thanks.

UPD: Here is the Plunker: https://plnkr.co/edit/HrIlOG9fsGhIS3w12bV0?p=preview

Answer

I've made an update to your plunkr: https://plnkr.co/edit/TCVkfJUDvLH68ybFJXqA?p=preview

The issue

The reason you got the message is because

  • You check the value of a property (like this.thisMonth)
  • You update the value of said property, thus changing it.
  • Angular in dev mode will run every check twice, to help you filter out possible bugs. (This fails in your case. Since you update a property on the class, if you run the checkMonth() function twice, it will yield two different results, one will be true, and the second will be false as you've already updated your value that you run the checks on.

In order to avoid the latter, you should separate the logic more (although I think the code you wrote makes perfect sense, and should be OK for production, but that's just not how Angular2 looks at it).

So instead of checking and setting properties "back and forth", you could for example write a function, that returns the same value, that's only based on the input parameter.


Possible solution

getShowableDays(weeks: Array): Array {
    let showableDays: Array<any> = [];
    let shownMonths: Array<number> = [];

    weeks.forEach((week) => {
      for(let day of week.days) {
        if(shownMonths.indexOf(day.getMonth()) === -1) {
          showableDays.push(day);
          shownMonths.push(day.getMonth());
        }
      }
    });

    return showableDays;
}

checkMonth(day): Boolean {
    let showableDays: Array<any> = this.getShowableDays(this.weeks);

    for(let showableDay of showableDays) {
      if(showableDay === day)
        return true;
    }

    return false;
  }

This will take your current weeks, and checks what the first occurences of each month are. Based on this, no matter how many times you run this code, the results will always be the same, as it will run independently from the rest of your application, or the application's state.