21

I am trying to create an observable that returns values when changes to a localStorage variable happens. My subscriber is not getting the new values upon changes to localStorage (or for that matter an in memory variable).

navbar.component.js

    import { Component, OnInit } from '@angular/core';
    import { UserService } from '../services/user.service';

    /**
     * This class represents the navigation bar component.
     */
    @Component({
      moduleId: module.id,
      selector: 'sd-navbar',
      templateUrl: 'navbar.component.html',
      styleUrls: ['navbar.component.css'],
      providers: [UserService]
    })

    export class NavbarComponent implements OnInit {
      loggedIn: boolean;
      constructor(private us: UserService) { }

      ngOnInit() {
        this.us.isLoggedIn().subscribe(loggedIn => {
          this.loggedIn = loggedIn;
        });
      }
    }

auth.component.ts

    import { Component, OnInit } from '@angular/core';
    import { ActivatedRoute } from '@angular/router';
    import { UserService } from '../shared/services/user.service';

    /**
     * This class represents the lazy loaded AuthComponent.
     */
    @Component({
      moduleId: module.id,
      selector: 'sd-auth',
      templateUrl: 'auth.component.html',
      styleUrls: ['auth.component.css'],
      providers: [UserService]
    })
    export class AuthComponent implements OnInit {
      authParams = {
        provider: '',
        params: {}
      };

      constructor(private route: ActivatedRoute, private us: UserService) { }
      ngOnInit() {
        this.route.params.forEach((param) => {
          this.authParams.provider = param.authprovider;
        });

        this.route.queryParams.forEach((queryParams) => {
          this.authParams.params = queryParams;
        });

        this.us.logIn("google", JSON.stringify(this.authParams));

        console.log(JSON.parse(localStorage.getItem('authParams')));

      }
    }

user.service.ts

    // user.service.ts
    import { Injectable } from '@angular/core';
    import { Observable } from 'rxjs/Observable';
    import { Subscriber } from 'rxjs/Subscriber';

    @Injectable()
    export class UserService {
      private loggedIn = false;
      private logger = new Observable<boolean>((observer: Subscriber<boolean>) => {
        observer.next(this.loggedIn);
      });
      constructor() {
        if (localStorage.getItem('authParams')) {
          this.loggedIn = !!JSON.parse(localStorage.getItem('authParams')).params.id_token;
        } else {
          this.loggedIn = false;
        }
      }

      logIn(provider: string, providerResponse: string) {
        localStorage.setItem('authParams', providerResponse);
        this.loggedIn = true;
      }

      isLoggedIn(): Observable<boolean> {
        return this.logger;
      }

      logOut() {
        localStorage.removeItem('authParams');
        this.loggedIn = false;
      }
    }

Flow looks like

Step 1- Navbar subscribes to UserService (Gets the default value of loggedIn=false) Step 2 - AuthComponent updates UserService (sets loggedIn = true)

My subscription in Navbar is not getting updated. What am I missing here. Do I need to put something in the logIn method of UserService like event emitter?

Collin Barrett
  • 2,441
  • 5
  • 32
  • 53
Prabhat
  • 4,066
  • 4
  • 34
  • 41
  • 3
    It doesn't matter if localstorage is used here or not. How can subscribers know that new value is avalable? `logIn` doesn't push new value to observable. A subject or event emitter would be appropriate here. – Estus Flask Nov 03 '16 at 05:01

4 Answers4

25

What you want is a Subject. Check out the docs here.

For a quick example, something like this:

export class UserService {
  ...
  private logger = new Subject<boolean>();
  ...

  isLoggedIn(): Observable<boolean> {
    return this.logger.asObservable();
  }

  logIn(provider: string, providerResponse: string) {
    localStorage.setItem('authParams', providerResponse);
    this.loggedIn = true;
    this.logger.next(this.loggedIn);
  }

  logOut() {
    localStorage.removeItem('authParams');
    this.loggedIn = false;
    this.logger.next(this.loggedIn);
  }
...
Milo
  • 3,365
  • 9
  • 30
  • 44
Fiddles
  • 2,790
  • 1
  • 32
  • 35
  • Hello, Fiddles I am completely agree with code base but I have a question as follow: 1) What if user maunally remove entry from localstorage, 2) What if user clear the browser history In such scenario can observable or listner works? If yes then how, please explain? – mangeshbhuskute Apr 01 '18 at 15:01
  • @mangesh localStorage does provide an events api for changes which you could subscribe to, but as you say, they could be changed manually, or the user could be logged out at the server. An approach you could take is to subscribe to http 401s, and update the logged in values accordingly/show a login form – Fiddles Apr 09 '18 at 14:22
  • @mangeshbhuskute I added an alternative answer which might be more like what you were thinking – Mike Miller Jul 26 '19 at 22:47
12

I wrote a StorageService to support Observable localStorage and sessionStorage. It supports both in one service.

StorageService

import { BehaviorSubject, Observable } from 'rxjs';

/**
 * Storage service
 * used for persist application data in observable key value pair
 */
export class StorageService {

    private storage: Storage;
    private subjects: Map<string, BehaviorSubject<any>>;

    /**
     * Constructor with service injection
     * @param storage 
     */
    constructor(storage: Storage) {
        this.storage = storage;
        this.subjects = new Map<string, BehaviorSubject<any>>();
    }

    /**
    * watch data of given key
    * @param key 
    * @param defaultValue 
    */
    watch(key: string): Observable<any> {
        if (!this.subjects.has(key)) {
            this.subjects.set(key, new BehaviorSubject<any>(null));
        }
        var item = this.storage.getItem(key);
        if (item === "undefined") {
            item = undefined;
        } else {
            item = JSON.parse(item);
        }
        this.subjects.get(key).next(item);
        return this.subjects.get(key).asObservable();
    }

    /**
     * get data of given key
     * @param key 
     */
    get(key: string): any {
        var item = this.storage.getItem(key);
        if (item === "undefined") {
            item = undefined;
        } else {
            item = JSON.parse(item);
        }
        return item;
    }

    /**
     * set value on given key
     * @param key 
     * @param value 
     */
    set(key: string, value: any) {
        this.storage.setItem(key, JSON.stringify(value));
        if (!this.subjects.has(key)) {
            this.subjects.set(key, new BehaviorSubject<any>(value));
        } else {
            this.subjects.get(key).next(value);
        }
    }

    /**
    * remove given key
    * @param key 
    */
    remove(key: string) {
        if (this.subjects.has(key)) {
            this.subjects.get(key).complete();
            this.subjects.delete(key);
        }
        this.storage.removeItem(key);
    }

    /**
     * clear all available keys
     */
    clear() {
        this.subjects.clear();
        this.storage.clear();
    }
}

LocalStorageService

import { Injectable, Inject } from '@angular/core';
import { StorageService } from './storage.service';

/**
 * Local storage service
 * used for persist application data in observable key value pair
 */
@Injectable()
export class LocalStorageService extends StorageService {

    /**
     * Constructor with service injection
     * @param window 
     */
    constructor(@Inject('WINDOW') private window: any) {
        super(window.localStorage);
    }
}

SessionStorageService

import { Injectable, Inject } from '@angular/core';
import { StorageService } from './storage.service';

/**
 * Session storage service
 * used for persist application data in observable key value pair
 */
@Injectable()
export class SessionStorageService extends StorageService {

    /**
     * Constructor with service injection
     * @param window 
     */
    constructor(@Inject('WINDOW') private window: any) {
        super(window.sessionStorage);
    }
}

This is how you can use the service:

import { LocalStorageService } from './local-storage.service';

export class TestClass implements OnInit, OnDestroy {

    constructor(
        private localStorage: LocalStorageService,
    ) { }

    ngOnInit() {
        // get current value
        this.localStorage.get('foo');

        // set new value
        this.localStorage.set('foo', 'bar');

        // watch value changes
        this.localStorage.watch('foo').pipe(takeUntil(this.unsubscribe)).subscribe(foo => console.log('foo changed', foo));
    }

    ngOnDestroy() {
        this.unsubscribe.next();
        this.unsubscribe.complete();
    }
}

(I'm really new to TypeScript with a couple of month experience. Any improvements or recommendations are welcome :-) )

blacktide
  • 10,654
  • 8
  • 33
  • 53
Alexander Bering
  • 603
  • 1
  • 6
  • 11
  • 2
    I did some modification to your code and replaced BehaviorSubject with ReplaySubject. :) https://stackblitz.com/edit/angular-ivy-9outi7?file=src%2Fapp%2Fservice.ts – aj go Dec 12 '20 at 10:57
5

An alternative is to observe the storage event

fromEvent(window, 'storage').subscribe((storageEvent) => {
  //do what you need to do with your storageEvent
})

This means you dont need to wrap the native API in any service layer.

TmTron
  • 17,012
  • 10
  • 94
  • 142
Mike Miller
  • 3,071
  • 3
  • 25
  • 32
  • 1
    This doesn't catch value changes made from within the same window. To catch the value changes the `setItem` and `getItem` functions of the storage have to bee intercepted. See my answer [here](https://stackoverflow.com/posts/63738297) – Mike Feustel Sep 04 '20 at 09:42
2

To be more precise, use BehaviourSubject from import { BehaviorSubject } from 'rxjs/BehaviorSubject';

For ex:

    @Injectable()
    export class UserService {

      public username = new BehaviorSubject<string>('');
      public preferredLanguage = new BehaviorSubject<string>('');
      public preferredTimezone = new BehaviorSubject<string>('');

      constructor(
        private router: Router,
        private jwtTokenService: JwtTokenService
      ) {
        let token: string = localStorage.getItem('token'); // handled for page hard refresh event
        if (token != null) {
          this.decode(token);
        }
      }

      private decode(token: string) {
        let jwt: any = this.jwtTokenService.decodeToken(token);
        this.username.next(jwt['sub']);
        this.preferredLanguage.next(jwt['preferredLanguage']);
        this.preferredTimezone.next(jwt['preferredTimezone']);
      }

      public setToken(token: any) {
        localStorage.setItem('auth_token', token);
        this.decode(token);
      }

    }

and to learn difference between BehaviorSubject and Observable: BehaviorSubject vs Observable?

Collin Barrett
  • 2,441
  • 5
  • 32
  • 53
virsha
  • 1,140
  • 4
  • 19
  • 40