Angular 7: How to send and receive a global event? - javascript

I study angular, sorry for the stupid question.
I need to make a service for windows with a single instance of the service or be able to send broadcast events and listen to them in each component.
Using #input and #output is not like There is a different nesting of components.
How to solve this issue?

One way to implement this is to use a service for that.
Inside the service create an Subject member variable (you need RxJS for it, which angular already installed because it's a dependency).
The Subject can be subscribed by any component outside the service (you'll need to inject the service in the component constructor).
Then, whenever you want to trigger that event you call .next() on the Subject and each subscriber will activate its own callback.
When subscription is done and not needed anymore it's important to .unsubscribe() from the Subject, otherwise there's a risk of memory leak (since the subscriber does not unsubscribe itself).
There are many examples out there, here is simple one.

Related

Contextual handling of RxJS Subject in Angular service

I have a hot RxJS Observable that I want to respond to in different ways depending on the context of the application. The Subject emits a new event based on some global action intercepted by a directive, but then I want
If a child component is subscribing to the Subject, then the child should handle the event
Otherwise, use a global handler
I can get the number of subscribers from the Subject and then tell the global handler to ignore if there are multiple subscribers, but it's not part of the API, so it seems like it may not be the right way to handle it. So what is the right way to do this?
Also, should the global event handler be part of the directive, the service, or should that be in a new component?
You can put global event subject in a global app.service and inject it in other component for subscription.
Although the ideal component should just have its own service maybe to handle complex event, but sometimes I feel directly inject a global service keeps the code cleaner. Otherwise, if you really want complete isolation or the component should be widely reusable e.g UI dropdown list, I suggest to use #Output to fire events (btw angular EventEmitter inherit Subject) and #Input to take in variables.

Dealing with state in a realtime messaging app

I'm looking for some guidance with regards to managing state in a real-time messaging/chat app built with VueJS 2.
The app consists of several components which are outlined in the following diagram:
So far, I've implemented displaying (fake) conversations. The App component contains an array with conversation objects. For each child component, the relevant data is passed using props. This is really simple and works like a charm.
Now, I have to deal with actions/mutations from components deeply nested in the tree. For example, sending a message and appending it to the corresponding array of messages.
I figured it would be as easy as dispatching a (global) event in the AppConversationChatWindowInput component and handling it in the App component. Boy was I wrong. Apparently, this functionality was removed when Vue 2.0 was introduced in favor of Vuex. I'm not sure why it was removed, because in some situations this could be a perfectly reasonable way to deal with events.
I guess there are a couple of possible solutions:
Passing the websocket connection to each child component. This could technically work. The App would connect to the websocket server and pass this connection to its child components using props. When the user sends a message, it is echoed by the websocket server. The App component can listen for the message and append it to the array of messages.
Regardless of the technical feasability, this feels like a crappy and hard-to-maintain archicture to me. In my opinion, no component other than the App should be aware of the websocket connection, let alone its concrete implementation.
Manually bubbling up the event in each component in the chain.
This seems like a complete pain to maintain. Introduces a lot of needless complexity and failure points.
Using a global event bus.
This is possible, but why should an input field depend upon a global event bus? I don't like unnecessary dependencies and coupling. It adds complexity and makes things harder to test.
Using a global data store (Vuex).
See #3. Another dependency and added complexity. Also, if I would settle for Vuex, how would I retrieve data in my components? Do I pass it down using components (like I do now) or would a component deep down in the tree just grab it from the store directly? To me, it feels like the component knows a lot more than it should this way.
Any thoughts? What's the best way to handle state in my situation?
There's a bit of a disconnect between "I wanted to dispatch a global event" and "I don't want to use a global event bus." A global event bus is how you dispatch/broadcast a global event. It is, as you note, a good solution in some situations. It is not hard to set up when you need it, so there's no strong reason for it to be in core Vue.
You can create the bus as an instance property on Vue so it is available to every component:
Vue.prototype.$globalEventBus = new Vue();
Where you would have had vm.$dispatch(...) you would do vm.$globalEventBus.$emit(...) and the receiving component can set up vm.$globalEventBus.$on(...).
Alternatively, you could create a bus at the top level and pass it through the children as a prop. This avoids globals, and you don't have to worry about bubbling.
Finally, as I noted in my comment, native events bubble, and you can catch them at any component higher up the chain. You could catch the event(s) that send messages, or even roll your own events to catch.

Angular 2 - single service provider across multiple browser windows

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

Update the DOM based on click event using service

I'm trying to update the DOM based on a click event, by using a service.
Specifically, I'm trying to use bootstrap's alerts to alert the user when they click on a particular button on the page. I'd like to be able to call notify.createNotification("Saved successfully") for instance, to tell the user that they have saved successfully.
I'm using ui-router and have managed to abstract the notification to a (isolate scope) directive and the accompanying template. The notification currently shows, updated, at the head of my application (all other views inherit this view) upon $scope.createNotification() (from within the click event). This works because the child views inherit the $scope property. Clearly though this pollution and abuse of the inheritance of the $scope is not ideal, hence I'd like to move it all into a service.
I've got as far as trying to use a factory to update the notification object, which contains state information for the alert, i.e. display:true/false, text etc. The trouble with the factory is it just returns where it's called from, I need to be able to update the parent.
...I feel I've done 9/10ths of the work on this, but that last 1/10th is really puzzling me.
Any help would be greatly appreciated.
Note: Guess I'm also looking for a 'best practices' here too, I mean $scope pollution works, but it's far from ideal. Thanks
This could be solved using pub/sub approach.
Create NotificationService that is used to send notifications. For callers it would look like NotificationService.alert({text: '..', ...})
Create <notification-area> component that subscribes to NotificationSerivce and displays notifications sent from anywhere.
NotificationService itself should implement pub/sub interface. Use any implementation of EventEmitter (like this one) or even angular.element to provide on(), off(), trigger() methods.

Angular services decoupling

I'm playing with angularjs doing some simple stuff. I wanted to incorporate some real time messaging like PubNub, PubSub or Pusher.
Basically I have it working, but it seems too cumbersome in some areas. Maybe you can show me a better way.
Ok, so here is my working jsfiddle (with pubsub faked) http://jsfiddle.net/canree/dD5VR/
I have external service (pubsub) and created angular service for it.
Now as I don't want other parts of my app to be dependent on this concrete implementation , I decided to create another one (inbox) that would store all the messages pubsub sent. And this inbox would be the dependency across my app (instead of pubsub).
So I want my dependencies to be:
controller -> inbox -> pubsub
When pubsub receives a message it needs to add it to inbox so that controller can see it and react accordingly. The problem is I don't want this backward dependency (pubsub -> inbox). I'd like pubsub to notify observers that message was received.
What I have now is ugly callback registration (inbox registers callback in pubsub). I don't like it very much as it has a lot of boilerplate (regarding this handling etc).
I wonder if there is other way to connect those services? Events (but without being forced to use $rootScope everywhere)? If so, where to register such event handler? Can services receive events?
Take a look at the fiddle provided and advice me how to solve it better way.
I don't want other parts of my app to be dependent on this concrete implementation
The interaction with the server is already wrapped/abstracted in the pubsub service. If the interaction with the server needs to change, it is already isolated to that service. I don't think adding another layer of abstraction buys you enough to offset the extra code you need to write (as you've already seen).
Here are a few other observations about how you could simplify your code (some of this you probably already know, but it might be instructive for others):
The content() function is not necessary (especially since messages is not private data). In Angular the controller $scope/model is often directly tied to a service model, so you could simply use:
$scope.messages = inbox.messages;
(If you like the content() function, consider making messages private in the inbox service.)
There is no need to send an event to the inbox controller about the change to inbox's messages array. The append() function just needs to call $rootScope.$digest(). And actually, this is probably not needed, since the real service would likely be using $http or $resource, which will automatically call $apply() or $digest. So _broadcastNewMessage() is not needed, nor is the $scope.$on() in the controller.
Even if you leave the content() function as is, you still won't need to broadcast anything.
Since inbox is dependent on pubsub, I would have the inbox service register itself
pubsub.subscribe(this.append, this);
pubsub's init() method can be in-line (no function required)
Fiddle with all of the above observations incorporated.
I wonder if there is other way to connect those services?
I can only think of model sharing (e.g., in service1: this.service1_obj_or_obj_property = service2.service2_obj_or_obj_property) and creating functions on the dependent service that the other service can call.
Can services receive events?
Since $on and $broadcast are defined on Scope, and since services are singletons and are not affected by scopes, I'm pretty sure the answer is "no."

Categories