How to get the LocalStorage value out of promise scope


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


Ionic get RESTful API issue
Services best practises
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
            })
        )