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).