11

I am trying to supply an alert once a task is complete - the user may be in any of multiple pages at the time. The alert should display to all pages.

I am using a service implementing BehaviorSubject The provider for which is in my app.component.ts page - single instance

In my app.component.html I have the two components, one the alert, the other that fires the alert.

<alert></alert>
<submit-service></submit-service>

The service emits to the alert component which renders the alert. This works fine, but only ever on the page that submits the service (not to any other page) - submission function is also in the alert component.

submit-service utilises public emit: BehaviorSubject<model> = new BehaviorSubject(new model());

Once the event is completed it then fires off this.emit.next(_model);

In the alert component I subscribe to the event

ngOnInit(): void {
    this.service.emit.subscribe(data=> {
           this.fireAlert(data);
        }
    });
}

so I suppose the main question is, how do I have a single service subscribed across multiple instances, across multiple pages?

EDIT 1

Apologies for the ambiguity, by page I mean separate browser window or tab i.e. window.open

Googs
  • 781
  • 6
  • 14
  • To my knowledge it's not really possible. You have to subscribe to the observable in each component that is "reacting" to new data, otherwise they wouldn't know that a change occured. So (if I am reading this right), you are doing it correctly. Can you clarify what it's doing that is not desireable? I can't quite discern that from your original post. – joshrathke Jul 28 '17 at 21:42
  • I guess the part I don't understand is "multiple pages". In an Angular context that's a little confusing. Do you mean multiple components deep within the application? – joshrathke Jul 28 '17 at 21:44
  • Sure, basically I am wanting to somehow extend the service and emission across multiple pages, not just multiple components within the same page. Even though I create a single instance of the provider, that single instance is only for the child components within that page. Each page then has its own instance of the parent (and therefore it's won subscribers). So wondering how I would go about creating a global instance across multiple pages, and if not possible, what kind of workarounds are possible or people may have done. – Googs Jul 28 '17 at 21:47
  • No, multiple physical pages, not child components – Googs Jul 28 '17 at 21:47
  • K, gotcha, answer coming.... – joshrathke Jul 28 '17 at 21:49
  • Thanks, thinking I have to go the route of a websocket instead, to which all pages have a listener, but hoping I can do it at application level... hoping... lol – Googs Jul 28 '17 at 21:51

2 Answers2

19

Just in case others are having this same issue - there is in fact an answer. Using global storage events allows the traversing of information across all browser tabs/windows.

In the service instead of using BehaviourSubject, the service updates or 'emits' the data to a local storage item, event listener utilising a HostListener() decorator can then listen for these udpates - which listens across all windows and tabs.

Example:

    @HostListener('window:storage', ['$event'])
    onStorageChange(ev: StorageEvent) {
       console.log(ev.key);
       console.log(ev.newValue);        
    }

See here for further information: Storage events

Googs
  • 781
  • 6
  • 14
  • Wow, nice find. This is definitely new information to me! Thanks for taking the time to update the answer. – joshrathke Aug 11 '17 at 17:14
  • Why not try using the [broadcastchannel](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel) ? – Swapnil Mhaske Apr 09 '19 at 07:20
  • @SwapnilMhaske BroadcastChannel is not supported in IE, Edge and Safari - but definitely a good idea if you are looking for Mozilla only supported option, good find. – Googs Apr 10 '19 at 16:15
  • Thanks for this. What if we wanted to emit our own event? So not onClick, etc, but rather `@Output() myCustomEvent: EventEmitter = new EventEmitter();` So every time I run `this.myCustomEvent.emit();`, I want HostListener to update localStorage. – Kyle Vassella Jun 20 '19 at 18:53
  • @KyleVassella you can update the storage whenever and however you wish, the listener will trigger whenever you change it (in other tabs/windows only). So in this case, when you emit the customEvent you can have a function that listens to that emitter and updates the storage event, that same function should then be triggered on the current page also so that it runs on all windows – Googs Jun 21 '19 at 19:51
  • This solution doesn't work for me. I used the service approach explained here https://stackoverflow.com/questions/35397198/how-can-i-watch-for-changes-to-localstorage-in-angular2 – Maurice Sep 02 '20 at 15:46
1

So there's a couple things at play here. The first is the service that let's your application know that it's time to display the alert. It sounds like you already have that, but for simplicity sake I would make sure you are declaring that in a forRoot() context. I won't go into a crazy amount of detail regarding this topic, but essentially you need to make sure that your service is running in the root context. If you start lazy loading modules, and then subscribing to your service from within the lazy loaded module, it will create it's own Dependency Injection context and you'll start pounding your head against the table wondering why your service isn't updating. (been there :)

The next thing to look at is where you want to render your alert. You'll likely want to use the ComponentFactoryResolver to render your alert in the highest level component you can think of that makes sense. Basically (if I understand your need correctly), you need this to be within the same component, or higher as all of the pages you want to have the alert rendered to. For example I am working on an application that has a dashboard where we have a ComponentFactoryResolver that renders any and all modals we might need throughout the application. This allows us to call modals from anywhere within the dashboard using, like you, a behavior subject that activates the modals. Here's a great article on using the ComponentFactoryResolver.

Update 1 So after realizing that "page" was actually a new browser window this method won't necessarily work. Using BehaviorSubjects will only update within the application context, so opening a new window creates a new application context, i.e. killing the BehaviorSubject of being a viable candidate to make this work. You'll need to have a service that is not instance specific. Web sockets as you mentioned would be a good alternative.

It is worth noting though that if it's possible to refactor the code to open modals instead of new windows, you could maintain the integrity of your Dependency Injection tree, and then use BehaviorSubjects to achieve this. Otherwise you'll need something outside of the application that is maintaining state.

joshrathke
  • 7,564
  • 7
  • 23
  • 38
  • I guess one question I have is are you talking about "pages" in the sense of multiple browsers open at the same time? I'm still not quite sure what a "page" is. – joshrathke Jul 28 '17 at 21:59
  • So from the dashboard there are a list of records, clicking one of these opens a new window (medical list opening a patient for example), which opens the patient component for that particular record (each record opens in a separate window). A function can be submitted from any of these patient windows, and once completed, the alert should display on not only the dashboard, and the record window that sent the request but all and any other open record windows as well. Hope that clears it up :) – Googs Jul 28 '17 at 22:21
  • So essentially by page I mean browser window i.e. `window.open` – Googs Jul 28 '17 at 22:22
  • Ahhhh. In that case I am afraid you are re-instantiating a new instance of the application with a separate DI context. Updating my answer... – joshrathke Jul 28 '17 at 22:25
  • No worries, I don't know if I would call my answer an "answer", but you are correct in that you'll need something beyond Angular to inform the multiple instances of the application of the state change. – joshrathke Jul 28 '17 at 22:30
  • Unfortunately Modal won't work as the different windows need to be controlled and maintained independently. I thought as much but thought I would double check if there wasn't something fancy out there I wasn't aware of, will have to create some web sockets, thanks for help, much appreciated – Googs Jul 28 '17 at 22:34