Variable in template doesn't always update

Hi,

Situation: I have an array in a provider that holds a list of objects. In the constructor of the page class, I link providers array (this.database.jobs) to a local class array (this.items):

  constructor(public navCtrl: NavController,
    public navParams: NavParams,
    private database: DatabaseProvider) {
    this.items = this.database.jobs;
  }

This is the relevant part of the template:

  <ion-list>
    <button ion-item *ngFor="let item of items" (click)="itemTapped($event, item)">
      {{item.name}}
    </button>
  </ion-list>

Scenario when testing in local browser (chrome via ionic serve):

  1. Page loads and the template shows the correct items
  2. I Update one item in this.database.jobs (in the background, not touching anything)
  3. Template updates as expected
  4. Update an item again in database.jobs
  5. Template doesn’t update

It does update if I click a button, resize the browserwindow or even log the value of this.items to console (see below) in the background.

  log() {
    setTimeout(() => {
      console.log(this.items);
      this.log();
    }, 3000);
  }

Anybody knows what’s going on here? Is this a bug? Do I make a fundamental mistake? Any pointers on where to look are very much appreciated.

Thanks.

I think this might be related to the zone not refreshing. Where you would have to use ngZone to handle your own zone refresh.

https://angular.io/api/core/NgZone

Angular change detection doesn’t fire after ionChange. If you’re changing variables that only change the contents of objects with ionChange, then you have to take more steps sometimes. I tend to use getters with Observables, so

*ngFor="let item of items | async"

get items(): Observable<ItemType> {
// emit updated version of the array here
}

If you don’t want to use an Observable: Depending on what else you’re doing in your ts file, you could use zone to force the change to occur inside the Angular zone (as suggested by the comment above), or inject ChangeDetectorRef and use the detectChanges() method there to force Angular to update.

1 Like

Thanks for your reply, @AaronSterling. Much appreciated!

I’ve implemented detectChanges(). I’m only able to inject it in the page object (and not in the dataprovider ‘onChange()’, which I would think of more suitable) Each second, the app detectChanges(). It works, but not the prettiest solution, IMO.

Using NgZone, as also suggested by @kgaspar (Thanks!) works as well, when I run the providers onChange() stuff in the zone:

          .on('change', (info) => {
            this._ngZone.run(() => {
              this.handleChange(info);
            })
            ...

But I also tried your first suggestion: Observables. I’m new to async programming and Angular, and I’m not sure if I understand the Observable principles completely. Using them (via subscribe) seems straightforward, though. I’ve created a setter which returns the array as an observable. But I still have the same problem as described above (Angular change detection doesn’t fire). Can you tell me what it is that I’m missing? Below is my relevant code. Thanks in advance.

View:

  <ion-list>
    <button ion-item *ngFor="let item of items | async">

Page:

...
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
...
export class ListPage {
  ...
  _items: Array<{ title: string, note: string, icon: string }>;
  get items(): Observable<Array<{ title: string, note: string, icon: string }>> {
    return Observable.of(this._items);
  }

  constructor(public navCtrl: NavController,
              private database: DatabaseProvider) {
    this._items = this.database.jobs;
  }
}

Does it take a while to resolve database.jobs? Angular change detection is more or less event-driven. Meaning when the page opens it runs a check, when you click a button it runs a check, etc. So maybe the page is running a check before database.jobs is downloaded, and then when the info is available, too late. Also, your Observable completes after emitting the current contents of this._items. If you want a stream that emits a new value every time this._items changes, you need to define it differently.

You should not need timeouts, or manually interacting with change detection or zones here. Please post the provider code that shows how jobs is populated and updated.

Any hints (link/pseudocode) on how to define it if I want a stream?

export class DatabaseProvider {
  public jobs: any;

This is my current solution (with NgZone, but it’s still a littlebit unclear why I need this). The onChange() fires when the database (pouchdb) receives changes.

        this._DB.sync(this._remoteDB, this._syncOpts)
          .on('change', (info) => {
            this._ngZone.run(() => {
              this.handleChange(info);
            })
            console.log('Handling syncing change');
            console.dir(info);
          })

This is the handleChange() function. It updates this.jobs


  handleChange(info) {
    for (let changed_doc of info.change.docs) {
      let originalDoc = null;
      let originalIndex = null;

      let index = this.jobs.findIndex(
        function(e) { return e._id === changed_doc._id }
      );
      let doc = this.jobs[index];
      if (doc) {
        originalDoc = doc;
        originalIndex = index;
      }
      //A document was deleted
      if (changed_doc.deleted) {
        this.jobs.splice(originalIndex, 1);
      } else {

        if (changed_doc._attachments) {
          this._DB.getAttachment(changed_doc._id, 'img.jpg').then((attachment) => {
            let url = window.URL.createObjectURL(attachment);
            changed_doc.image = url
            if (originalDoc) {
              this.jobs[originalIndex] = changed_doc;
            } else {
              this.jobs.push(changed_doc);
            }
          });
        } else {
          //A document was updated
          if (originalDoc) {
            this.jobs[originalIndex] = changed_doc;
          } else {
            //A document was added
            //this.notification.notification('A new job was added');
            this.jobs.push(changed_doc);
          }
        }
      }
    }
  }

This seems specific to PouchDB, with which I have no experience. Perhaps somebody who uses it can comment.