Service in tabbed pages doesn't work, but works correctly other single pages


#1

I have a service for sqlite storage in a contacts application. The homepage contains 2 tabs, one for Favorites, and one for User Groups. You can search a username from either of these tabs to view a user’s contact information page. The problem is that although a user’s contact info page can successfully use the service to check if it has been favorited or not, the Favorites page can’t use the exact same service to list all favorites, using the exact same code. Are tabbed pages different somehow? For a very simple example (relevant code only):

From the contact info page (this works):

import {Page, NavController, NavParams} from 'ionic-angular';
import {Component} from '@angular/core';
import {StorageProviderService} from '../../providers/storage-provider-service/storage-provider-service';

@Component({
  templateUrl: 'build/pages/employee-view/employee-view.html'
})

export class EmployeeViewPage {

  constructor(public nav: NavController, public navparams : NavParams, public employeeSearchService: EmployeeSearchService, public storageProviderService : StorageProviderService) {
         this.storageProviderService = storageProviderService;
         console.log(this.storageProviderService.getAllGroupsVariable());
         console.log(this.storageProviderService.getAllFavsVariable())
}

That works just fine.

Now from the Favorites, which is one of the tabbed home pages (the folders are on the same level, so the import is successful)

import {Page, NavController, NavParams} from 'ionic-angular';
import {Component} from '@angular/core';
import {StorageProviderService} from '../../providers/storage-provider-service/storage-provider-service';

@Component({
  templateUrl: 'build/pages/Favorites/Favorites.html'
})

export class Favorites { 
  constructor(public nav : NavController, public employeeSearchService: EmployeeSearchService, public storageProviderService : StorageProviderService) {
      this.storageProviderService = storageProviderService;
      console.log(this.storageProviderService.getAllFavsVariable())
          console.log(this.storageProviderService.getAllGroupsVariable());
  }

These console.logs return undefined. Exact same service, exact same code.

Is there an issue with tabbed views? Am I missing something?


#2

I see two things, not sure how important:

  • EmployeeSearchService does not seem to be imported
  • The explicit assignment of this.storageProviderServvice is unnecessary

#3

Sorry about that, EmployeeSearchService is imported, I just omitted the import statement to keep the post concise.
In fact, its leftover from a previous experiment, and isn’t even used in this class. I’ll remove it.

I did find something else that is strange though… If I attach a function to a button that calls that variable returned by the service, I can get it just fine. Secondly, I can ngFor successfully on the View if I do something like this: *ngFor=“let fav of getAllFavs()”, where getAllFavs() returns that variable (its an array).

However, I can’t do this in the constructor: favorites = getAllFavs(), and then do this in the View: “let fav or favorites”.

Why can I get to it if I execute it as a function in the template, but not the constructor? I should mention that the service implements its return values as promises, and I considered that perhaps a race condition might have occurred that caused the console.logs to be undefined, but should’t the ngFor in the view update when the promise resolves?

ps… I"m also still confused about when explicit assignments (as you mentioned) are necessary, and when they aren’t. I’m just following the this.nav = nav example when I inject my own services.


#4

Would you mind showing that code? I suspect that it’s not really returning what you intend it to.

Not needed when you put an access control modifier (public or private) on a constructor parameter. An object variable will be automatically declared and initialized for you.


#5

Absolutely, and thanks

Here is the relevant code in the StorageProviderService

private static allFavs : any;

//the constructor below uses the sqlite plugin
  constructor(public platform : Platform) {
      this.platform.ready().then(() => {
          this.storage = new Storage(SqlStorage);
      });
    }

//get data from sqlite
    getFavUsers(){
        return new Promise((resolve, reject)=>{
            let readQuery = `SELECT * FROM favorites`;
            this.storage.query(readQuery).then((data)=>{
                let dataArray = [];
                for(let i = 0; i < data.res.rows.length; i++){
                    dataArray.push(data.res.rows.item(i));
                }
                resolve(dataArray);
            });
        });
    }

//getters & setters to update the allFavs variable, which is initialized in app.ts

    getAllFavsVariable(){return StorageProviderService.allFavs;}
    setAllFavsVariable(){this.getFavUsers().then((data)=>{StorageProviderService.allFavs = data})}

/*In the Favorites component, I used to originally just call getFavUsers(), but it didn't work (worked fine in the other views).  I then decided to get the db data into a variable (favorites), and initialize it in the app.ts constructor, thinking maybe it the tabs view was being created before getFavUsers() resolved:*/

the following is in app.ts constructor

storageProviderService.setAllFavsVariable();

//again, All other components have no issues.  Just the tabbed components.

#6

I think this is the crux of the problem. Just because it’s kicked off there, you have no guarantee that StorageProviderService.allFavs is set by the time you try to read it.

getFavUsers is kind of a mess. You don’t need to be explicitly constructing another promise when you already have one, and SELECT * is bad because it acts differently when you change the schema.

I would make allFavs a ReplaySubject:

export class StorageProviderService {
  private _allFavs: Subject<Favorite[]> = new ReplaySubject<Favorite[]>(1);

  // call this from platform.ready().then() or any other time you want
  fetch(storage:Storage):void {
    storage.query(`SQL`).then((dbrv) => {
      let favs = convertDatabaseResultsToFavoritesArray(dbrv);
      this._allFavs.next(favs);
    });
  }

 // expose as supertype to allow implementation changes
  getAllFavs():Observable<Favorite[]>() {
    return this._allFavs;
  }
}

Now in consumers, we can do this:

@Component({
  pipes: [AsyncPipe]
})
export class FavoritesPage {
  public favs:Observable<Favorite[]>;

  constructor(sps:StorageProviderService) {
    this.favs = sps.getAllFavs();
  }
}
<ul>
  <template ngFor let-fav [ngForOf]="favs | async">
    <li>{{fav}}</li>
  </template>
</ul>

#7

Thanks. I guess I’m going to have to book-up on RxJS. As usual, I didn’t know how much I don’t know!


#8

Well, after looking at this more closely today with fresh eyes, I think I might understand why the question above didn’t work. My original logic was to assign do the assignment in Favorites arbitrarily expecting it to update when the promise eventually resolved. However, since the value was ‘undefined’ at assignment time, it was passed by value rather than a reference to the future array. When the array finally resolved, my assignment in Favorites didn’t know anything about it. At least, that is what my rookie programmer mind came up with.


#9

This is precisely what the Subject does: allows you to subscribe to changes whenever the promise resolves. It’s also not limited to firing a single time: I use this idiom a lot when managing data that comes from backend API requests. It greatly simplifies the typical “press a refresh button and magically get the data where it needs to go” feature desire.