4

I want to render a visual countdown timer. I'm using this component, https://github.com/crisbeto/angular-svg-round-progressbar, which relies on SimpleChange to deliver changes.current.previousValue & changes.current.currentValue.

Here is the template code

<ion-card  class="timer"
  *ngFor="let snapshot of timers | snapshot"
> 
  <ion-card-content>
    <round-progress 
      [current]="snapshot.remaining" 
      [max]="snapshot.duration"
      [rounded]="true"
      [responsive]="true"
      [duration]="800"
      >
    </round-progress>
</ion-card>

I'm using this code to trigger angular2 change detection

  this._tickIntervalHandler = setInterval( ()=>{
      // run change detection
      return;
    }
    // interval = 1000ms
    , interval
  )

updated (After much testing, I've discovered that the problem isn't the precision of the time I am rendering, and this question has been changed to reflect that.)

The problem is that ngFor is called multiple times inside 1 change detection loop. Regardless of my tick interval or the precision of snapshot.remaining (i.e. seconds or tenths of seconds) if snapshot.remaining happens to change on a subsequent call to ngFor during change detection, I get an exception:

Expression has changed after it was checked

If I render only a single timer without using ngFor then change detection works fine--even for intervals of 10ms.

How can I render multiple timers on a page, presumably using ngFor, without triggering this exception?

Solution

After a bit of testing, it seems the problem is using a SnapshotPipe with ngFor to capture the snapshot of the Timer data. What finally worked is to take the snapshot of the Timer data in the View Component. As mentioned in the answer below, this uses a pull method to get changes, instead of a push method.

    // timers.ts
    export class TimerPage {
        // use with ngFor
        snapshots: Array<any> = [];

        constructor(timerSvc: TimerSvc){
            let self = this;
            timerSvc.setIntervalCallback = function(){
                self.snapshots = self.timers.map( (timer)=>timer.getSnapshot()  );
            }
        }

    }


    // timer.html
    <ion-card  class="timer"  *ngFor="let snapshot of snapshots"> 
    <ion-card-content>
        <round-progress 
        [current]="snapshot.remaining" 
        [max]="snapshot.duration"
        [rounded]="true"
        [responsive]="true"
        [duration]="800"
        >
        </round-progress>
    </ion-card>



    // TimerSvc can start the tickInterval
    export class TimerSvc {
        _tickIntervalCallback: ()=>void;
        _tickIntervalHandler: number;

        setIntervalCallback( cb: ()=>void) {
            this._tickIntervalCallback = cb;
        }

        startTicking(interval:number=100){
            this._tickIntervalHandler = setInterval( 
                this._tickIntervalCallback
                , interval
            );
        }
    }
michael
  • 4,377
  • 8
  • 47
  • 73

1 Answers1

2

The first suspicious thing is toJSON (it has wrong name or returns string or converts string to array), which objects are contained in timers array?

During change detection for loop might be evaluated multiple times so it should not generate new objects. Also toJSON pipe should be marked as pure (see Pipe decorator options). Also it is better to provide trackBy function for ngFor. In general it is better in this case to update class field instead of using pipe.


public snapshots:any[] = [];

private timers:any[] = [];

setInterval(()=>{
    this.snapshots = this.updateSnapshots(this.timers);
}, 100);

<ion-card  class="timer"
  *ngFor="let snapshot of snapshots"
> 
kemsky
  • 14,727
  • 3
  • 32
  • 51
  • `toJSON` could easily be renamed `snapshot`, it returns a simple Object with `snapshot.remaining` in seconds with different levels of precision. I added a `trackByFn` but that did not solve the problem. `pure:true` does not help because `SimpleChange` needs the object reference to remain the **SAME** in order to capture `changes.current.previousValue` which equals `snapshot.remaining`. The problem is that the timer is ticking away in `MS` during the `ngFor` loop. As I type this, I'm thinking that a hack could be to debounce `snaphshot`... – michael Feb 16 '17 at 07:18
  • I mean `throttle`, and I tried `throttle` at different values, but no luck. Also, it doesn't matter how fast the `ngFor` loop is, because as you mentioned, the change detection loop seems to call `ngFor` multiple times. if the timing value happens to change the error is thrown. This is true even if the precision is 1 second. – michael Feb 16 '17 at 08:07
  • If I take the timer out of the `ngFor` I can render the timer every 10MS without an error. Updating the question to reflect this fact – michael Feb 16 '17 at 08:43
  • I think in this case you should run `setInterval` in template owner compnent, and update snapshot array in callback, in other words push new values rather than let angular pull new values at random times. – kemsky Feb 16 '17 at 18:32
  • That works!!! It's not as cleanly encapsulated, but it does seem to work. I can use `ngFor` and still render 100ths of a second with this approach. I'm going to wait a little bit to see if there is another approach this is better encapsulated with the Timer--but if not I will accept your answer. – michael Feb 16 '17 at 19:46