Reputation: 343
I'm trying to filter existing objects of type Worklog and summarize the time spent on each one in my PeriodViewTable component. I'm having a problem with duplicate method calls.
I've tried to fix the problem with resetting the value in ngOnInit(), but it only partially solves the problem - the user sees the correct value, but it's still wrong behind the scenes and console outputs "ExpressionChangedAfterItHasBeenCheckedError". What is more, the methods are recalled each time I click on DatePicker elements in Form component - which is a different one.
Here's my PeriodViewTable component:
<div *ngIf="datesBetween.length > 0 && (filterType == 6 || filterType == 5)">
<div class="container container-fluid">
<mat-checkbox #hide [checked]="true">
<label>Show overview</label>
</mat-checkbox>
</div>
<table *ngIf="hide.checked" class="table table-bordered table-striped">
<thead>
<tr>
<th>Issue</th>
<th *ngFor="let date of datesBetween">{{date}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let wrk of worklogs">
<td>{{wrk.issueKey}}</td>
<td *ngFor="let date of datesBetween">{{getWorklogTimeForDay(wrk, datesBetween.indexOf(date))}}</td>
</tr>
<tr>
<th>Total</th>
<th *ngFor="let date of datesBetween">{{getTotalTimeForDay(datesBetween.indexOf(date))}}</th>
</tr>
<tr>
<th>Total for period</th>
<th>{{periodTotal}}</th>
</tr>
</tbody>
</table>
</div>
So for example, if I have 5 entries in the worklogs
array and selected 2 days, the getWorklogTimeForDay
method should be called 10 times - for each entry in the array * amount of days. Also, the getTotalTimeForDay
method should be called once for each day, so here 2 times total. For some reason, the process is done twice. As I said before, it's also triggered after I click on any of the DatePicker HTML elements in a different component - Form component. This moment is where I'm totally lost. Here's its HTML code:
<div class="container container-fluid">
<div class="form">
<div class="row">
<div class="col-md-4">
<div class="input-group">
<input #tfDomain type="text" class="form-control" placeholder="Domain address" value="jira-test">
<div class="input-group-append">
<span class="input-group-text">.atlassian.net</span>
</div>
</div>
</div>
<div class="col-md-4">
<input #tfProject type="text" class="form-control" placeholder="Type project name here" value="TEST">
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="input-group">
<input #tfUser type="text" class="form-control" placeholder="Email address" value="user.name">
<div class="input-group-append">
<span class="input-group-text">@testmail.com</span>
</div>
</div>
</div>
<div class="col-md-4">
<input #pfPassword type="password" class="form-control" placeholder="Password" value="passwrd123">
</div>
</div>
<h4>Filters</h4>
<div class="row">
<div class="col-md-4">
<div class="input-group">
<input #userFilter type="text" class="form-control" placeholder="Email address" value="user.filter"
[disabled]="startDate.value.length>0 && chbRefresh.checked">
<div class="input-group-append">
<span class="input-group-text">@testmail.com</span>
</div>
</div>
</div>
<div class="col-md-6">
<mat-form-field>
<input #startDate matInput [matDatepicker]="picker" placeholder="Start date"
[disabled]="userFilter.value.length>0 && chbRefresh.checked">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<mat-form-field>
<input #endDate matInput [matDatepicker]="picker2" placeholder="End date"
[disabled]="userFilter.value.length>0 && chbRefresh.checked">
<mat-datepicker-toggle matSuffix [for]="picker2"></mat-datepicker-toggle>
<mat-datepicker #picker2></mat-datepicker>
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-xs-6">
<button class="btn btn-primary" id="searchButton"
(click)="submitAll(tfDomain.value, tfProject.value, tfUser.value, pfPassword.value, chbRefresh.checked, userFilter.value, startDate.value, endDate.value)">
Search
</button>
</div>
<div class="col-xs-6">
<mat-checkbox #chbRefresh [disabled]="userFilter.value.length > 0 && startDate.value.length > 0"
[checked]="true">
<label>Refresh search</label>
</mat-checkbox>
</div>
</div>
</div>
</div>
The PeriodViewTable typescript file:
export class PeriodViewTableComponent implements OnInit {
datesBetween: string[];
worklogs: Worklog[] = [];
newSearch: boolean;
filterType: number = 0;
periodTotal: number = 0;
someint: number = 0;
someint2: number = 0;
constructor(private formService: FormService) { }
ngOnInit() {
console.log('ngOnInit() - period view table');
this.formService.newSearchChanged.subscribe(
newSearch => {
if(newSearch != this.newSearch) {
this.worklogs = [];
this.periodTotal = 0;
}
}
);
this.formService.filterChanged.subscribe(filterType => this.filterType = filterType);
console.log('filterChanged');
this.datesBetween = this.formService.datesBetweenChanged.subscribe( dates => this.datesBetween = dates);
this.formService.worklogsChanged.subscribe(worklog => this.worklogs.push(worklog));
}
getWorklogTimeForDay(wrk: Worklog, index: number){
console.log('getWorklogTimeForDay(' + wrk + ', ' + index + ')'+ ' ' + this.someint++);
let currentDate = new Date(this.datesBetween[index]);
if(moment(wrk.started).startOf('day').isSame(moment(currentDate).startOf('day'))) {
return wrk.timeSpentSeconds/3600;
}else return '-';
}
getTotalTimeForDay(index: number) {
console.log('getTotalTimeForDay(' + index + ')' + ' ' + this.someint2++);
let total: number = 0;
for(let wrk of this.worklogs){
if(moment(wrk.started).startOf('day').isSame(moment(this.datesBetween[index]).startOf('day'))){
total += wrk.timeSpentSeconds/3600;
}
}
this.periodTotal += total;
return total;
}
}
Below is link to the screenshot of what I see in the console. As you can see, the process is repeated twice: https://i.sstatic.net/nZS5V.jpg And how it looks like, visualized: https://i.sstatic.net/uj3HC.jpg https://i.sstatic.net/yrM8h.jpg
Upvotes: 1
Views: 166
Reputation: 359
Angular provides whole life cycle of component and change detection mechanism. If you have any getter / setter or function on your template, it will be called with every change detection cycle. And Angular runs a lot of that cycles. That's why you should never have any getter, setter or function on template - remember that. You should only bind properties (which are variables which are not functions). If that data does not change during component's life, calculate it inside ngOnInit()
.
For me, you should definitely calculate everything you need to even before *ngFor
is being called (like for example, after receiving data from observable).
Recommending How does Angular2 change detection really works?, it should get you understand the topic.
Upvotes: 0
Reputation: 3004
The reason both methods are triggered more often than they supposed to is that {{someMethodCall()}}
is triggered each time the component is rendered. So on changing anything in the component - the methods are being called.
Here's a quick example
I would advice you not to use methods inside string interpolation.
On how to fix this problem - please consider creating a custom pipe for each of the methods you need. The pipe will be triggered only if the data is changed, thus giving you the expected behavior.
UPDATE
You can also use getters instead of pipes, since some pipes are discouraged – @trichetriche
Upvotes: 1