ngFor not updating automatically

Hello everybody. I’m new to the ionic framework, so my problem will probably have a very simple solution. Like the title explains, my ngFor in the HTML code isn’t updating automatically when its bound array is updated. It only gets updated when I interact with a graphic element.

My problem is very similar to a problem that users of ionic 2 beta had a year ago, but that problem should have been fixed since then according to the posts regarding that topic. https://forum.ionicframework.com/t/ngfor-not-updating/50184

my code:

page.ts

import { Component, NgZone } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';

import { UsersService } from '../../providers/users-service';
import { GamesService } from '../../providers/games-service';


@Component({
  selector: 'page-friends',
  templateUrl: 'friends.html'
})
export class FriendsPage {

  private friendList= [];
  private zone: any;

  constructor(public navCtrl: NavController, 
  public navParams: NavParams, 
  private usersService: UsersService,
  private gamesService: GamesService) {
    this.zone = new NgZone({enableLongStackTrace: false})
    this.createList();
  }

  ionViewDidLoad() {
    console.log('ionViewDidLoad FriendsPage');
  }

  createList(){
    this.usersService.getCurrentUser().then((user) => {  
      if(user.val()){
        for(let userGroup in user.val().usergroups){
          this.usersService.getUsersFromClass(userGroup, this.userUpdateCallBackFunction);
        }
      }
    });

  }

  userUpdateCallBackFunction = (users: any) => {
    this.friendList.length = 0; 
    let that = this


      users.forEach(function(user) {
        that.friendList.push(user);
    });
  }

page.html

<ion-header>

  <ion-navbar>
    <ion-title>Friends</ion-title>
  </ion-navbar>

</ion-header>


<ion-content padding>

  
 <ion-card *ngFor="let friend of friendList">

  <ion-item >
    <ion-avatar item-left>
      <ion-img  src="{{friend.val().photo}}"></ion-img>
    </ion-avatar>
    <h3>{{friend.val().username}}</h3>
     <button ion-button item-right (click)="friendClicked(friend)"><ion-icon name="game-controller-b"></ion-icon></button>
    <!--<p>2pm today</p>-->
  </ion-item>


  <!--<ion-card-content>
    <p>{{friend.username}}</p>
  </ion-card-content>-->

</ion-card>

</ion-content>

provider.ts

  getUsersFromClass(userGroupId: any, callBackFunction: any){
    let friendsRef = this.userProfile.orderByChild("usergroups/" + userGroupId).equalTo(true);
    friendsRef.on('value', function(snapshot) {
      callBackFunction(snapshot);
    });
  }

In the console, I can see the array getting filled, but like I said previously, the GUI doesn’t update until interacted with.
I tried using:

that.zone.run(()=>{
  users.forEach(function(user) {
    //console.log(user.key().val());
    that.friendList.push(user);
  });
});

and that works, but from what I understand it is bad practice to use ngZone.run() so I would like to have a working codebase without the ngZone.

1 Like

Don’t use callbacks. Restructure getUsersFromClass to return an Observable<User[]> instead of taking a callback and everything will work as expected.

Thank you rapropos for the quick response. I’ve been searching and trying for a while now and I understand observables are the way to go in angular 2 (to which I am also new) with regards to communication between pages and providers. However I can’t seem to get it to work. Could you maybe provide (part of) the restructured provider?

Guessing from the function names that you’re using Firebase, so I would start by looking at the angularfire2 docs. That library should do much of the heavy lifting for you as far as accessing stuff as Observables.

I am not using angularfire but the node module “firebase”.

I adapted my code to work work with subscriptions and not with callbacks but to no avail. (Thank you rapropos for making me aware of subscriptions.) I will post my current code and I will put a link to youtube where I show the exact behavior. I modified the code a bit (removed the non-relevant functions) to improve readability.

friends.ts

import { Component } from '@angular/core';
import { NavController, NavParams, App } from 'ionic-angular';
import { Subscription } from 'rxjs/Rx';

import { UsersService } from '../../providers/users-service';
import { GamesService } from '../../providers/games-service';

import { UserModel } from "../../models/user-model";
import { GameModel } from "../../models/game-model";

import { QuestionPage } from '../question/question';



@Component({
  selector: 'page-friends',
  templateUrl: 'friends.html'
})
export class FriendsPage {

  private userGroupUsers: UserModel[] = [];
  private usersFromUserGroupSubscription: Subscription;

  constructor(public navCtrl: NavController, 
  public navParams: NavParams, 
  private usersService: UsersService,
  private gamesService: GamesService,
  private app: App) { }

  ngOnInit(){
    this.subscribeToUsersFromUserGroup();
  }

  subscribeToUsersFromUserGroup(){
    this.usersService.getCurrentUser().then((user) => {  
      if(user){
        for(let userGroup of user.usergroups){
          this.usersFromUserGroupSubscription = this.usersService.subscribeToUsersFromUserGroup(userGroup).subscribe(
            user => {
              let index: number = this.userGroupUsers.findIndex(x => x.ID  == user.ID);
              if(index == -1){
                console.log('user added');
                console.log(user);                
                this.userGroupUsers.push(user);
              }else{
                this.userGroupUsers[index] = user;
              }
            });
        }
      }
    });
  }

  userGroupUserClicked(userGroupUser: UserModel){ }

  ngOnDestroy(){
    this.usersFromUserGroupSubscription.unsubscribe();
  }
}

friends.html

  <ion-navbar>
    <ion-title>Friends</ion-title>
  </ion-navbar>

</ion-header>


<ion-content padding>

  
 <ion-card *ngFor="let userGroupUser of userGroupUsers">

  <ion-item >
    <ion-avatar item-left>
      <ion-img  src="{{userGroupUser.photo}}"></ion-img>
    </ion-avatar>
    <h3>{{userGroupUser.username}}</h3>
     <button ion-button item-right (click)="userGroupUserClicked(userGroupUser)"><ion-icon name="game-controller-b"></ion-icon></button>
    <!--<p>2pm today</p>-->
  </ion-item>


  <!--<ion-card-content>
    <p>{{friend.username}}</p>
  </ion-card-content>-->

</ion-card>

</ion-content>

user-model.ts

import { GameModel } from "./game-model";

export class UserModel {

    public ID: string;
    public username: string;
    public photo: string;
    public usergroups: string[] = [];
    public games: GameModel[] = [];

    constructor(){

    }

    parseFirebaseSnapshotToUser(userId: string, value: any){
        this.ID = userId;
        for(let usergroup in value.usergroups){
            this.usergroups.push(usergroup)
        }
        for(let game in value.games){
            let gameModel = new GameModel();
            gameModel.ID = game;
            this.games.push(gameModel);
        }
        this.username = value.username;
        this.photo = value.photo;
    }
}

user-service.ts

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import {Subject, Observable} from 'rxjs/Rx';

import * as firebase from 'firebase';

import { UserModel } from "../models/user-model";

@Injectable()
export class UsersService {

  public fireAuth: any;
  public userProfile: any;
  public userUid: any;

  constructor(public http: Http) {
    this.fireAuth = firebase.auth();
    this.userProfile = firebase.database().ref('users');
  }

  static getCurrentUserId(){
    let fireBaseUser = firebase.auth().currentUser;
    if(fireBaseUser){
      return fireBaseUser.uid;
    }
  }

  getCurrentUser() : firebase.Promise<UserModel>{
    let currentUserId = UsersService.getCurrentUserId();
    return this.getUser(currentUserId);
  }

  subscribeToCurrentUserGameIds() : [Observable<firebase.database.DataSnapshot>, Observable<firebase.database.DataSnapshot>]{
    let currentUserId = UsersService.getCurrentUserId();
    const subjectChildAdded = new Subject<firebase.database.DataSnapshot>();
    const subjectChildRemoved = new Subject<firebase.database.DataSnapshot>();
    if(currentUserId){
      firebase.database().ref('users/' + currentUserId + '/games/').on('child_added', function(snapshot) {
        subjectChildAdded.next(snapshot);
      });
      firebase.database().ref('users/' + currentUserId + '/games/').on('child_removed', function(snapshot) {
        subjectChildRemoved.next(snapshot);
      });
    }
    return [subjectChildAdded, subjectChildRemoved]
  }

  subscribeToUsersFromUserGroup(userGroupId: string) : Observable<UserModel>{
    const subjectClassUser = new Subject<UserModel>();
    let friendsRef = this.userProfile.orderByChild("usergroups/" + userGroupId).equalTo(true);
    friendsRef.on('value', function(snapshot) {
      let value = snapshot.val()
      for(let userId in value){
        let userModel: UserModel = new UserModel()
        userModel.parseFirebaseSnapshotToUser(userId, value[userId])
        subjectClassUser.next(userModel)
      }
    });
    return subjectClassUser;
  } 

  getUser(userId: any) : firebase.Promise<UserModel>{
    return firebase.database().ref('users/' + userId).once('value').then((snapshot) => {
      let userModel: UserModel = new UserModel();
      userModel.parseFirebaseSnapshotToUser(snapshot.key, snapshot.val());
      return userModel;
    });
  }
}

If there are any questions… shoot.

As promised in the previous post: a youtube video outlining the undesirable behavior:

You can see the logs changing. These are the logs that are added in the Class: FriendsPage => Function: subscribeToUsersFromUserGroup()

After a random amount of time (a few seconds to more than a minute) the list of friends appears. If I however interact with the GUI in any way the list also appears (e.g. go to Home and back to the tab Friends).