13

I can't seem to get angular2 view to be updated on an array.push function, called upon from a setInterval async operation.

the code is from this angular plunkr example of setInterval:

What i'm trying to do is as follows:

import {View, Component, bootstrap, Directive, ChangeDetectionStrategy, ChangeDetectorRef} from 'angular2/angular2'

@Component({selector: 'cmp', changeDetection: ChangeDetectionStrategy.OnPush})
@View({template: `Number of ticks: {{numberOfTicks}}`})
class Cmp {
  numberOfTicks = [];
  
  constructor(private ref: ChangeDetectorRef) {
    setInterval(() => {
      this.numberOfTicks.push(3);
      this.ref.markForCheck();
    }, 1000);
  }
}

@Component({
  selector: 'app',
  changeDetection: ChangeDetectionStrategy.OnPush
})
@View({
  template: `
    <cmp><cmp>
  `,
  directives: [Cmp]
})
class App {
}

bootstrap(App);
<!DOCTYPE html>
<html>

<head>
  <title>angular2 playground</title>
  <script src="https://code.angularjs.org/tools/traceur-runtime.js"></script>
  <script src="https://code.angularjs.org/tools/system.js"></script>
  <script src="https://code.angularjs.org/tools/typescript.js"></script>
  <script data-require="jasmine" data-semver="2.2.1" src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.js"></script>
  <script data-require="jasmine" data-semver="2.2.1" src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine-html.js"></script>
  <script data-require="jasmine@*" data-semver="2.2.1" src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/boot.js"></script>
  
  <script src="config.js"></script>
  <script src="https://code.angularjs.org/2.0.0-alpha.37/angular2.min.js"></script>
  <script>
    System.import('app')
      .catch(console.error.bind(console));
  </script>

</head>

<body>
  <app></app>
</body>

</html>

The above code will work properly if "numberOfTicks" is just a number, (as the plunker original example shows) but once I change it to an array and push data, it won't update.

I can't seem to understand why is that.

The following behaviour is similar to an issue I have when trying to update a graph in angular2 with new data points when using setInterval / setTimeout.

Thanks for the help.

Omer Kushmaro
  • 195
  • 1
  • 1
  • 6
  • I'll elaborate - I'm trying to update a graph in realtime by updating it's data array using the above setInterval methodoligy, this plunker might be a good example to try and achieve this: http://plnkr.co/edit/vdgKVJOymMYhiyqZrPma?p=preview – Omer Kushmaro Mar 27 '16 at 13:21

2 Answers2

20

You need to update the whole reference of your array after adding an element in it:

  constructor(private ref: ChangeDetectorRef) {
    setInterval(() => {
      this.numberOfTicks.push(3);
      this.numberOfTicks = this.numberOfTicks.slice();
      this.ref.markForCheck();
    }, 1000);
  }
}

Edit

The DoCheck interface could also interest you since it allows you to plug your own change detection algorithm.

See this link for more details:

Here is a sample:

@Component({
  selector: 'custom-check',
  template: `
    <ul>
      <li *ngFor="#line of logs">{{line}}</li>
    </ul>`
})
class CustomCheckComponent implements DoCheck {
  @Input() list: any[];
  differ: any;
  logs = [];

  constructor(differs: IterableDiffers) {
    this.differ = differs.find([]).create(null);
  }

  ngDoCheck() {
    var changes = this.differ.diff(this.list);
    if (changes) {
      changes.forEachAddedItem(r => this.logs.push('added ' + r.item));
      changes.forEachRemovedItem(r => this.logs.push('removed ' + r.item))
    }
  }
}
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • Thanks Thierry, while this indeed solves the case that i've described here, i'll elaborate a bit more. I'm trying to update a graph dynamically by updating its value array, things is, if I slice the array the graph will be redrawn entirely. In angular 1.x i did this using $timeout/$interval which run the $apply and make angular to detect changes. I'm trying to achieve the same 'realtime' affect using angular 2 – Omer Kushmaro Mar 27 '16 at 12:49
  • I'll rephrase - using the method you suggested in a graph will work, only the graph would 'flash' rather then just drawing a new point on the existing graph – Omer Kushmaro Mar 27 '16 at 13:02
  • Okay I understand. Perhaps you could inject a ChangeDetectorRef instance in your component and call its markForCheck method after added an element in the array. – Thierry Templier Mar 27 '16 at 13:06
  • Yeah thats exactly what i'm trying to do, unfortunately it seems like angular still requires the array reference to change. However, the behavior i'm seeing is very strange. Angular will update the graph if I do any action on the browser. Things like resizing browser size / opening & closing debugger. As long as I do these, the graph magically updates with the same behavior as in angular 1. – Omer Kushmaro Mar 27 '16 at 13:13
  • Yes that's the case for perfoémane reasons. I asked them in the past and they told me that I could use the approach used the NgStyle directive. See rhis link: https://github.com/angular/angular/blob/master/modules/angular2/src/common/directives/ng_style.ts. – Thierry Templier Mar 27 '16 at 13:20
  • The DoCheck interface could interest you since it allows you to plug your own change detection algorithm. – Thierry Templier Mar 27 '16 at 13:25
  • 2
    @ThierryTemplier https://angular.io/docs/ts/latest/api/core/DoCheck-interface.html ==> 404 && https://github.com/angular/angular/blob/master/modules/angular2/src/common/directives/ng_style.ts ===> 404 – Ajey Mar 06 '17 at 19:15
9

The above code will work properly if "numberOfTicks" is just a number, but once I change it to an array and push data, it won't update. I can't seem to understand why is that.

It is because a number is a JavaScript primitive type, and an array is a JavaScript reference type. When Angular change detection runs, it uses === to determine if a template binding has changed.

If {{numberOfTicks}} is a primitive type, === compares the current and previous values. So the values 0 and 1 are different.

If {{numberOfTicks}} is a reference type, === compares the current and previous (reference) values. So if the array reference didn't change, Angular won't detect a change. In your case, using push(), the array reference isn't changing.

If you put the following in your template instead

<div *ngFor="#tick of numberOfTicks">{{tick}}</div>

then Angular would update the view because there is a binding to each array element, rather then just the array itself. So if a new item is push()ed, or an existing item is deleted or changed, all of these changes will be detected.

So in your chart plunker, the following should update when the contents of the from and to arrays change:

<span *ngFor="#item of from">{{item}}</span>
<span *ngFor="#item of to">{{item}}</span>

Well, it will update if each item is a primitive type.

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492