How to get the LocalStorage value out of promise scope


#11

You’d do something like this:

var startPromise = this.local.get('start_time');
var stopPromise = this.local.get('stop_time');

Promise.all([startPromise, stopPromise]).then((start_time, stop_time) => {
var elapsed_time = stop_time - start_time;
...
});

(I apologize for any typos, it’s a tad difficult to type code on my phone!)


#12

Hello.
Your problem here is a mix of synchronous and asynchronous code.

There is no grantee that retrieving from locsl storafe finish before the next statement. In fact it doesn’t. Read this as “when is ready please call this callback which has something to do”

At first promise call you just register a callback to be called next time when the promise is evaluated.

Then the next statement is evaluated.

After your code end them the promise is evaluated and retrieve the local storage value.

Set a breakpoint in each statement in order to see execution flow.


#13

Should be:
Promise.all([startPromise, stopPromise]).then((arrayOfResults) => { ...


#14

Thank you. This solution solved my issue. I think I understand Promises a little better now.


#15

@rapropos, my problem is that am trying to get the data in localstorage and sent to mongodb, but the property is empty or something it does not just get to the db. have tried consoling but the log is {"__zone_symbol__state":true,"__zone_symbol__value":“data”}. when I tried the promise part, no luck. please suggest way i can get the data to the db. thanks


#16

@rapropos, i solved the issue, have to wrap the the promise call with the http call to nodejs, and that worked
let data= this.storage.get(‘data-from-localstorage’)
data.then(request =>{
let _data= data

this.service.add( _data)

})


#17

I have the following Load() method working but I would like to write something less repetitive and compact when loading several user options…

//
import { Injectable } from '@angular/core' // 20170515
import { Storage } from '@ionic/storage' // 20170515

//
@Injectable()
// 20170515
export class UserOptionsService {
    //
    constructor(
            public Storage: Storage
    ) {
        console.log("UserOptionsService------------>>Created");

        // TODO: how to handle this correctly since services are not expected to deal directly with views?
        // if page is destroyed like browser refresh, tab closed, etc...
        window.addEventListener('beforeunload', () => {
            this.Save();
            return false;
        });
    }

    //----------------------------------------------------------------------
    KeepMediaPlaying: boolean = true;
    ContinuousPlaying: boolean = true;
    PlayWhileHidden: boolean = true;
...
Load() {
        console.log("UserOptionsService------------>>Load");
        this.Storage.ready().then(() => {
            this.Storage.get("KeepMediaPlaying").then((data) => {
                this.KeepMediaPlaying = data;
                console.log("UserOptionsService<<------------Storage.get ", data);
            })
                .catch(() => {
                    console.log("UserOptionsService------------>>Load DEFAULTS");
                    this.KeepMediaPlaying = true;
                });

            this.Storage.get("ContinuousPlaying").then((data) => {
                this.ContinuousPlaying = data;
                console.log("UserOptionsService<<------------Storage.get ", data);
            })
                .catch(() => {
                    console.log("UserOptionsService------------>>Load DEFAULTS");
                    this.ContinuousPlaying = false;
                });

        });
    }

So I attempted to factor out the Storage.get like the following:

 private StorageGet(Key: string, Default: any): any {
        this.Storage.get(Key).then((data) => {
            console.log("UserOptionsService<<------------Storage.get ", Key, data);
            return (data);
        })
            .catch(() => {
                console.log("UserOptionsService------------>>Load DEFAULTS", Default);
                return Default;
            });
    }

and call my StorageGet() like so

  Load() {
        console.log("UserOptionsService------------>>Load");
        this.Storage.ready().then(() => {

            this.KeepMediaPlaying = this.StorageGet("KeepMediaPlaying", true);
            this.ContinuousPlaying = this.StorageGet("ContinuousPlaying", true);
            this.PlayWhileHidden = this.StorageGet("PlayWhileHidden ", false);
        });
    }

However, after the change above, when I enter my settings view of toggles they do not reflect the loaded values from storage until the second visit.
I don’t know how to to apply the Promise.All( …) to my case.
Can you please assist me in coding the “promises/resolve” correctly? Just when I think I understand them, they bite me again, I am lost :slight_smile:

The view component does just the following

 // 20170515
    ionViewWillEnter() {
        console.log("UserOptions<<------------ionViewWillEnter");
        this.UserOptionsService.Load();
    }
    // 20170515    
    ionViewDidLeave() {
        console.log("UserOptions<<------------ionViewDidLeave");
        this.UserOptionsService.Save();
    }

and the view html (template) looks like this:

        <ion-item>
            <ion-label>Keep Media Playing</ion-label>
            <ion-toggle [(ngModel)]="UserOptionsService.KeepMediaPlaying"></ion-toggle>
        </ion-item>

        <ion-item>
            <ion-label>Continuous Playing</ion-label>
            <ion-toggle [(ngModel)]="UserOptionsService.ContinuousPlaying"></ion-toggle>
        </ion-item>

        <ion-item>
            <ion-label>Play While Hidden</ion-label>
            <ion-toggle [(ngModel)]="UserOptionsService.PlayWhileHidden"></ion-toggle>
        </ion-item>

Thank you.

** NOTE: ** every app developer will sooner or later deal with storage, if you happen to know of a pattern, starter or template, we could all follow it, it would cut on the noise multi posts such trivial topics generate. By all means, please include a link.

While I wait for the darn pizza to be prepared…
How do I capture ALL promises?
When ALL promises are resolved, how do I tell the view to refresh itself?

  • via posting an event from the service to the page?
  • how will the view refresh its toggles controls upon subscribe to such an event?
  • Isn’t ngModel in the view supposed to take care of that?

Thanks.


#18

While I could not wait for that promised pizza, I cooked up a different pizza

Like so

    private StorageGet(Key: string, Default: any) {

        return new Promise((resolve, reject) => {
            this.Storage.get(Key).then((data) => {
                console.log("UserOptionsService<<------------Storage.get ", Key, data);
                resolve(data);
            })
                .catch(() => {
                    console.log("UserOptionsService------------>>Load DEFAULTS", Default);
                    resolve(Default);
                });
        });
    }

and call it like this

    Load() {
        console.log("UserOptionsService------------>>Load");
        this.Storage.ready().then(() => {
this.StorageGet("KeepMediaPlaying", true).then((data:boolean) => {this.KeepMediaPlaying = data});
            this.StorageGet("ContinuousPlaying", true).then((data:boolean) => {this.ContinuousPlaying = data});
                
        });
    }

Even though it appear to be causing the view load the toggles matching the loaded value from the storage (dumped to console), IOW, it’s working, I don’t know what I am doing :slight_smile: just hacking away…

I have also tried pure guess something like this, which also seems to work

            Promise.all([
                this.StorageGet("KeepMediaPlaying", true).then((data: boolean) => { this.KeepMediaPlaying = data })
                , this.StorageGet("ContinuousPlaying", true).then((data: boolean) => { this.ContinuousPlaying = data })
            ]);

Ideally, I would like to have a boolean flag in the above service let’s say “Ready” on the which the view can hide the toggles and switch to a spinner with a couple of *ngIf, in case the view races and paints before the service gets to load the data from whatever storage.


#19

Ok, here is a late night pizza… and it is working

            Promise.all([
                this.StorageGet("KeepMediaPlaying", true).then((data: boolean) => { this.KeepMediaPlaying = data })
                , this.StorageGet("ContinuousPlaying", true).then((data: boolean) => { this.ContinuousPlaying = data })
            ])
                .then(() => { this.Ready = true });

the view does this…it shows a spinner in case the service is slowly roasting that pizza to an extra crisp…

<!---->
<ion-content padding>
    <!--http://ionicframework.com/docs/api/components/spinner/Spinner/ -->
    <ion-spinner *ngIf="!UserOptionsService.Ready" name="bubbles"></ion-spinner>
    <div *ngIf="UserOptionsService.Ready">

        <!--http://ionicframework.com/docs/api/components/toolbar/Toolbar/ -->
        <!--http://ionicframework.com/docs/components/#toggle -->
        <!--https://webcake.co/binding-data-to-toggles-and-checkboxes-in-ionic-2/ -->
        <!--https://forum.ionicframework.com/t/ionic-2-beta9-toggle-ionchange/55535 -->
        <ion-item>
            <ion-label>Keep Media Playing</ion-label>
            <ion-toggle [(ngModel)]="UserOptionsService.KeepMediaPlaying"></ion-toggle>
        </ion-item>

        <ion-item>
            <ion-label>Continuous Playing</ion-label>
            <ion-toggle [(ngModel)]="UserOptionsService.ContinuousPlaying"></ion-toggle>
        </ion-item>
    </div>
</ion-content>

I hope this is what I am supposed to be coding like ?-(
BTW, I faked a delay in my StorageGet() and I get the expected behavior and results of the view.
So Promise.All( ) is doing a parallel wait on all the calls and returns when all resolve or reject.

Now, I need to fix, handle that unexpected browser refresh, close tab, thingy…
How, can I avoid doing that kind of handling in every view if service is not the place?
Have all views derive from a base class? Please raise your hand, before I cook another pizza!


#20

Is there a reason you’re splitting all these things up? I would just do this:

export interface Preferences {
  playContinuously: boolean;
  keepMediaPlaying: boolean;
  playWhileHidden: boolean;
}

export class PreferenceService {
  preferences: Promise<Preferences>;

  constructor(private _storage: Storage) {
    this.preferences = _storage.ready().then(() => _storage.get('preferences'));
  }

  setPreferences(prefs: Preferences): Promise<void> {
    return this._storage.set('preferences', prefs).then(() => {
      this.preferences = Promise.resolve(prefs);
    });
  }
}

export class SettingsPage {
  preferences: Preferences;

  constructor(private _prefService: PreferenceService) {
    _prefService.preferences.then(prefs => this.preferences = prefs);
  }

  save(): void {
    this._prefService.setPreferences(this.preferences);
  }
}

Synchronization between different components
Provider/server: Provide the data or contain the data?
#21

Not really, I did not pay attention that storage.set/get can gobble up a whole object :slight_smile: Thanks for the code template.

I noticed you cache preferences in the service for the purpose of loading them, then later copied over by the page.
Then you send the cached copy in the page to the service to be saved.
What’s the advantage of doing this way, instead of keeping the data only in one place, like in the service?

Oh I think in my case, the view will be manipulating the live and only copy of the settings.
Thanks


#22

A couple of things.

Exposing the object as a Promise instead of a raw Preferences means we can get away without having a separate ready guard for the service. I figure if we’re exposing a ready promise and then separately a raw object, that is just inviting clients to make the mistake of failing to wait on ready and trying to access the data before it’s in a reliable state. Exposing only a Promise protects me from being able to screw up in this way. This structure also gives us the benefit for free of only having to hit storage once, and we don’t have to have any of that gnarly if (data) { return Promise.resolve(data) } else { return new Promise((resolve, reject) => blablabla } that gives me headaches. Once stuff has come back from the initial read, our exposed Promise is in a resolved state and the service has absolutely nothing else that it needs to do.

Now, we need a way to save new values. Yes, it would be feasible to have stash() take no arguments and simply save the state of an object kept in the service, but then we have lost the elegance of exposing only a Promise and will have to keep a separate raw Preferences object. Having the same data in two places is an incitement to bugs about what happens when they don’t agree, so it’s something I try my best to avoid. Since a function call to the service is going to have to be made anyway, why not just have it take an argument representing the new Preferences?

And that’s how I ended up with this idiom of exposing only a Promise in the service and storing the actual object in pages that are interested in it.


Services best practises
Ionic get RESTful API issue
How to skip signup/login if there is already a user session object
#23

I follow your design and I do concur with choices and like your arguments especially about the if’s promises… part.


#24
  1. How do I write my *ngIf expression in terms of the interface obtained in the page thru the promise?. Originally I was expressing *ngIf via implementing a .Ready either on the page or the service. Now, I am trying the following but was not sure if I am using correct typescript to handle the case when the data is returned null.
    <ion-spinner *ngIf="!UserOptions" name="bubbles"></ion-spinner>

    <div *ngIf="UserOptions">

        <ion-item>
            <ion-label>Keep Media Playing</ion-label>
            <ion-toggle [(ngModel)]="UserOptions.KeepMediaPlaying"></ion-toggle>
        </ion-item>

        <ion-item>
            <ion-label>Play Continuously</ion-label>
            <ion-toggle [(ngModel)]="UserOptions.PlayContinuously"></ion-toggle>
        </ion-item>
    </div>

I have never used interfaces without explicit objects that actually implement those interfaces (at least in C# and my limited coding).
If I use your service template constructor above as is, the promise returns a null the very first time from the storage and the user options page will only show the spinner forever, given the way I coded the *ngIf.

In your approach, How do you return a default object?

In order to get the option knobs to display in the UserOptionsPage to display I found myself doing the following:

// 20170523
export interface UserOptions_I {
    PlayContinuously: boolean;
    KeepMediaPlaying: boolean;
    PlayWhileHidden: boolean;
}
// 20170525
export class UserOptions implements UserOptions_I {
    PlayContinuously: boolean = true ;
    KeepMediaPlaying: boolean = true ;
    PlayWhileHidden: boolean = true ;
    
}

and changing your single line in SettingsPage constructor from

UserOptionsService.UserOptions_P.then(data => this.UserOptions = data);

to

export class UserOptionsPage {

UserOptions: UserOptions_I;

constructor(
        public NavController: NavController
        , public UserOptionsService: UserOptionsService
    ) {
        console.log("UserOptionsPage<<e------------Created");
        this.UserOptionsService.UserOptions_P.then((data) => {            
            if (data != null) {
                console.log("UserOptions.ctor<<P------------UserOptions", data);
                this.UserOptions = data;
            }
            else {
                console.log("UserOptions.ctor<<P------------UserOptions", "Load DEFAULTS");
                this.UserOptions = new UserOptions();           
            }   
                 
        });
}
  1. Given that you only want to expose a Promise of Preferences, does that mean every client creates its own cache of the object? For example, another page like Page2, needing to consume some of the preferences, would do the same in its constructor
UserOptionsService.UserOptions_P.then(data => this.UserOptions = data);

Wouldn’t that create multiple copies of the preference? and How would the preferences be broadcasted / updated in Page2 if the user accessed and made changes in the settings page while the page2 was already created and remained around in another tab.

I want to know if I am on the right track and if you have another more compact approach of achieving the same.
Thanks for sharing your thoughts.


#25

Googling “create object from interface in typescript” I found there are few syntax to do this in the TypeScript world.
So I got rid of having an explicit object/class implementing the interface and I moved this default loading into the service like so:

UserOptions_P: Promise<UserOptions_I>;

// called from the constructor:
Load() {
        console.log("UserOptionsService------------>>Load");
        this.UserOptions_P = this.Storage.ready().then(() => this.Storage.get("UserOptions"))
        // 
        this.UserOptions_P.then((data) => {
            //
            if (data != null) {
                console.log("UserOptionsService.ctor<<P------------UserOptions", data);
            }
            else {
                console.log("UserOptionsService.ctor<<P------------UserOptions", "Load DEFAULTS");
                data = <UserOptions_I>{
                    PlayWhileHidden: true,
                    PlayContinuously: true,
                    KeepMediaPlaying: true
                };
            }
        });
    }

Will clean some more…


#26

I get the following error

ERROR TypeError: Cannot read property ‘SomeNewOption’ of null

When using the following to access UserOptions.SomeNewOption. This can happen when some members of my UserOptions are not present in the local storage. I think because I added them and they have not been saved previously eg. SomeNewOption below .

export interface UserOptions_I {
    PlayContinuously: boolean;
    KeepMediaPlaying: boolean;
    PlayWhileHidden: boolean;
    SomeNewOption: boolean;
}

@Injectable()
export class UserOptionsService {

UserOptions_P: Promise<UserOptions_I>;

    constructor(
        public Storage: Storage
    ) {
        console.log("UserOptionsService------------>>Load");
        this.UserOptions_P = this.Storage.ready().then(() => this.Storage.get("UserOptions"))
   }

  Save(UserOptions: UserOptions_I) {
        console.log("UserOptionsService------------>>Save");
        return this.Storage.set("UserOptions", UserOptions).then(() => {
            this.UserOptions_P = Promise.resolve(UserOptions);
        });
    }
}

Using a try catch below works but I was wondering if there is a better way to handle this

export class SomePage {

UserOptions: UserOptions_I;

    constructor(
        public UserOptionsService: UserOptionsService
    ) {
            this.UserOptionsService.UserOptions_P.then((data) => {
            console.log("UserOptions.ctor<<P------------UserOptions", data);
            this.UserOptions = data;
        });
    }

  ionViewDidEnter() {
   try {
        if (UserOptions.SomeNewOption === false)                
            something...
        } catch (error) {
            console.log("UserOptionPage------------>>CATCH on cannot read SomeNewOption ");
            something...
        }

   }
}

I have tried coding without try-catch using !! operator like so

 if ( !! UserOptions.SomeNewOption) ...

or === or !== to handle null but none of these worked and I still get the exception (ERROR mentioned above).

What is the recommended coding to initialize UserOptions object/interface to my desired defaults in case 1) either all of UserOptions is not present in the storage or 2) only some members are not present.

Thank you.


#27

If this is the actual code, you never assign to UserOptions of the page. You instead are assigning to UserOptions_I. Your naming conventions are very unusual. Normally, classes are PascalCase, with no underscores and no I indicating interface. Properties are camelCase, which makes it impossible to have clashes with class names.

That being said, you simply can’t do what you are trying to do. You cannot rely on a future having been resolved at the time a lifecycle event is called. Initialize userOptions to a sane dummy value (like {}) in an initializer, do whatever needs to be done once it’s resolved inside that then block, and let Angular’s change detection take care of the rest.


#28

Nice catch about only assigning to UserOptions_I, that was a typo introduced by my posting, VSCode won’t let me get away with that :).
Sorry to put you thru my naming style, I am aware of the JS/TS conventions but I am stubborn like a goat when it comes to naming anything including other environments like writing dates in the following format YYYYMMDD on checks, tax return, applications :).

I have hard time making sense of things and at least initially, I like to name them what they are and match their case with their types: Storage : Storage (it hasn’t bit me too much).

  • UserOptions is my object/data
  • UserOptions_I is an interface to my object
  • UserOptions_P is a promise of my object

I don’t like it when I am cornered to name the same thing two different ways just to avoid the naming space. So, I prefer if it is all Preferences, UserSettings or all UserOptions and from there I have:

  • UserOptionsPage
  • UserOptionsService
  • etc…

I dislike very much case-sensitive languages and environments (Unix included), but welcome case aware ones. Case sensitivity made sense like 40 years ago when compilers/computers where slow and memory was scarce. Today, I feel it’s ridiculous and causes more harm than benefits. That’s just my opinion.

Ideally, with a case-insensitive language/OS/FileSystem, a language-aware IDE would infer types and could show them as pseudo suffix or prefixes pined or on mouse hover. All based on user settings, the same goes for tabs, indentation and identifier capitalization (source code would be stored in some standard and rendered in IDE) Rendering it in this forum would meet your settings camelCase, PascalCase, etc… Of course I will long gone before this happens :slight_smile:

Again, thank you for putting up with my convention. Back to our topic…

In order to handle initialization when there is no UsersOptions in the storage I unwrapped your single liner

into the following

which causes the following flow of events, notice how GlobalService promise somehow resolves to undefined before UserOptionsService one

However, when I code/guess it like the following:

Then, the flow of events makes sense and GlobalService accesses a defined copy of the already resolved UserOptions from UserOptionsService

If there is a better way to code it let me know .

Finally, I tested when no data is present in the storage, and the service loads a default object {}. Again things look OK and resolve nicely.

At the moment, it looks like it’s working well and all clients retrieve the promise from the service as a resolved object even if it just initialized to {}.

Earlier, I tried to initialize the service promise member with something like:

UserOptions_P: Promise<UserOptions_I> = new Promise<UserOptions_I>((resolve) => { resolve() });

Without handling in the .then(( but it did not seem to work.

Earlier, due to my lack of knowledge of JS/TS, I was chasing the wrong thought that somehow, the code was failing because the storage had some members of the User Option structure but not others. That was not the case.
I know I have to re-visit Promises in more depth as I only have a shallow understanding.

Thanks again.


#29

One rule of thumb that will make your code clearer and more concise is: don’t instantiate them yourself (with new Promise()). It is extremely rare that you would ever need to do so. Almost always, you are starting out an operation by calling some framework function that is giving you one, and just keep chaining then() off of it. If you get an Observable, and want to convert it to a Promise, use toPromise(). fromPromise() works in the other direction.


#30

Thanks, I simplified the user options service constructor to

this.UserOptions_P = this.Storage.ready().then(() =>
            this.Storage.get("UserOptions").then((data) => {
                console.log("UserOptionsService.ctor<<P------------UserOptions", data);
                if (!data) {
                    data = <UserOptions_I>{};
                    console.log("UserOptionsService.ctor<<P------------Load DEFAULTS", data);
                }
                return data
            })
        )