Publish Subscribe service using AngularJS - javascript

I am using this code to create a factory for publishing and subscribing messages between controllers , directives and services .
angular.module('app', []);
angular.module('app').controller('TheCtrl', function($scope, NotifyingService) {
$scope.notifications = 0;
$scope.notify = function() {
NotifyingService.publish();
};
// ... stuff ...
NotifyingService.subscribe($scope, function somethingChanged() {
// Handle notification
$scope.notifications++;
});
});
angular.module('app').factory('NotifyingService', function($rootScope) {
return {
subscribe: function(scope, callback) {
var handler = $rootScope.$on('notifying-service-event', callback);
scope.$on('$destroy', handler);
},
publish: function() {
$rootScope.$emit('notifying-service-event');
}
};
});
It is working fine but I want to pass data while I am publishing to someone whos subscribing that ,how do I do that.
Suppose I want to publish a value of 4 , How do I perform that?

If I understood correctly you want to publish value 4 to the 'notifying-service-event' and you want to use that value inside the subscriber.
In order to publish a value you need to pass it to your emit function.
publish: function(msg) {
$rootScope.$emit('notifying-service-event', msg);
}
Then, when you are using this publish function, pass the value you want.
node.on("click", click);
function click() {
NotifyingService.publish(4);
}
While handling the subscribe event:
NotifyingService.subscribe($scope, function somethingChanged(event,msg) {
console.log(msg); //4
scope.number = msg //or whatever you want
scope.$apply();
});
you can find a full example here: https://plnkr.co/edit/CnoTA0kyW7hWWjI6DspS?p=preview
which was an answer to the question:
Display informations about a bubble chart D3.js in AngularJS

Related

Using http requests, promises, ng-options, and factories or services together

I'm trying to retrieve a list of options from our database and I'm trying to use angular to do it. I've never used services before but I know that's going to be the best way to accomplish what I want if I'm going to use data from my object in other controllers on the page.
I followed a couple tutorials and put together a factory that makes an http request and returns the data. I've tried several ways of doing it, but for some reason nothing is happening. It's like it never runs the factory function and I can't figure out why.
Factory:
resortModule= angular.module('resortApp',[]);
resortModule.factory('locaService',['$http', function ($http){
var locaService= {};
locaService.locations = {};
var resorts = {};
locaService.getLocations=
function() {
$http.get('/url/url/dest/').success(function (data) {
locaService.locations = data;
});
return locaService.locations;
};
return locaService;
//This is a function I would like to run in addition to the first one so multiple variables would be stored and accessible
/*getResorts:
function(destination) {
$http.get('/url/url/dest/' + destination.id).success(function (data) {
resorts = data;
});
return resorts;
}*/
}]);
resortModule.controller('queryController',['$scope', 'locaService', function($scope, locaService) {
$scope.checkConditional= function (){
if($("#location").val() == ""){
$("#location").css('border','2px solid #EC7C22');
}
};
$scope.selectCheck= function (){
$("#location").css('border','2px solid #ffffff');
$(".conditional-check").hide();
};
$scope.resort;
$scope.locations= locaService.getLocations();
}]);
I just want the data to be returned and then assigned to the $scope.locations to be used for ng-options in the view. Then I want my other function to run on click for the next field to be populated by the variable resort. How would I do this? Any help would be great! Thanks!
$http service returns a promise, and your function should return that promise. Basically your getLocations function should be something like the following
locaService.getLocations=
function() {
return $http.get('/url/url/dest/');
};
Then in your controller you should retrieve the options using this promise:
locaService.getLocations()
.then(
function(locations) // $http returned a successful result
{$scope.locations = locations;}
,function(err){console.log(err)} // incase $http created an error, log the returned error);
Using jquery in controllers or manipulating dom elements in controllers is not a good practice, you can apply styles and css classes directly in views using ng-style or ng-class.
Here is an example how all it should look wired up:
resortModule= angular.module('resortApp',[]);
resortModule.factory('locaService',['$http', function ($http){
var locaService= {
locations: {}
};
var resorts = {};
locaService.getLocations= function() {
return $http.get('/url/url/dest/');
};
return locaService;
//This is a function I would like to run in addition to the first one so multiple variables would be stored and accessible
/*getResorts:
function(destination) {
$http.get('/url/url/dest/' + destination.id).success(function (data) {
resorts = data;
});
return resorts;
}*/
}]);
resortModule.controller('queryController',['$scope', 'locaService', function($scope, locaService) {
/* Apply these styles in html using ng-style
$scope.checkConditional= function (){
if($("#location").val() == ""){
$("#location").css('border','2px solid #EC7C22');
}
};
$scope.selectCheck= function (){
$("#location").css('border','2px solid #ffffff');
$(".conditional-check").hide();
};
*/
$scope.resort;
locaService.getLocations()
.then(
function(locations) // $http returned a successful result
{$scope.locations = locations;}
,function(err){console.log(err)} // incase $http created an error, log the returned error);
}]);

AngularJS how to update view/scope after event listener

I have on my controller and service like this (both on separate file):
.controller('authCtrl',['$scope','MyConnect',function($scope,MyConnect){
/***************Testing Area******************/
console.log("connecting");
MyConnect.initialize();
$scope.myID = ??? //I want this to be updated
}
.factory('MyConnect', ['$q', function($q) {
var miconnect = {
initialize: function() {
this.bindEvents();
},
bindEvents: function() {
document.addEventListener('deviceready', this.onDeviceReady, false);
},
onDeviceReady: function() {
thirdPartyLib.initialize();
miconnect.applyConfig();
},
applyConfig: function() {
if (thirdPartyLib.isReady()) {
//I want in here to update $scope.myID in controller and reflect the changes in UI textbox
//$scope.myID = thirdPartyLib.id(); //something like this will be good
}
else {
}
}
}
return miconnect;
}])
So, I'm not sure how to update $scope.myID (which is a textbox). I'm not sure how to do the callback after event listener.. usually if ajax I can use .then to wait for the data to arrive.
Main thing is, I need to use 3rd party library (proprietary), and based on the guide is, to call thirdPartyLib.initialize() after device ready, then check if that thirdPartyLib.isReady() before actually calling the function to retrive the id.
You can't directly assign to $scope.myID until your service is ready. You need to somehow provide a callback that will assign the correct value to your $scope model. You could do this either by making the service return a Promise somewhere that resolves when it's ready, or by emitting an event from the service. I'll give an example of the last option. Depending on how much this thirdPartyLib is integrated with angular your may need to kick angular to get the scope to apply properly. Here I use $scope.$evalAsync. You could also return a promise that will resolve with the id rather than passing a callback directly in order to .then like you would with an ajax library.
Also, if the thirdPartyLib is particularly sucky, and it's initialize is asynchronous, and it doesn't provide you any callback/promise/event driven indicator that it's ready, you may need to
.controller('authCtrl', ['$scope', 'MyConnect',
function($scope, MyConnect) {
console.log("connecting");
// my connect should probably just `initialize()` on it's own when it's created rather than relying on the controller to kick it.
MyConnect.initialize();
MyConnect.whenID(function(id) {
// $evalAsync will apply later in the current $digest cycle, or make a new one if necessary
$scope.$evalAsync(function(){
$scope.myID = id;
});
})
}
])
.factory('MyConnect', ['$q', '$rootScope'
function($q, $rootScope) {
var miconnect = {
...,
onDeviceReady: function() {
thirdPartyLib.initialize();
miconnect.applyConfig();
/* Also, if the `thirdPartyLib` is particularly sucky, AND if it's initialize is asynchronous,
* AND it doesn't provide any callback/promise/event driven indicator that it's ready,
* you may need to hack some kind of `setTimeout` to check for when it is actually `isReady`. */
// ok, listeners can do stuff with our data
$rootScope.$emit('MyConnect.ready');
},
whenID: function(callback) {
if (thirdPartyLib.isReady()) {
callback(thirdPartyLib.id);
} else {
var unregister = $rootScope.$on('MyConnect.ready', function() {
// unregister the event listener so it doesn't keep triggering the callback
unregister();
callback(thirdPartyLib.id);
});
}
}
}
return miconnect;
}
])

Angular, "activate" a watch after data has come in

I am trying to have a $watch on a scope so I Can listen for changes, however, I do not want the watch to start listening until the data has come in and populated. I am using a $q factory and then to populate the items, then I want the watch to start listening after everything is populated. I can't seem to get down how to control these order of events.
SO I have in my controller -
//call to the $q factoy to execut all http calls
getDefaults.resource.then(function(data){
//fill in scopes with data
$scope.allAccounts = data[0].data.accounts;
//THEN watch the scope for changes
$scope.$watch('selectedResources', function (newValue) {
//do action on change here
});
}
SO I'm wondering if there is any way to control these order of events in angular. Thanks for reading!
You can create a service for your xhr calls:
.factory('xhrService', function ($http) {
return {
getData: function () {
return $http.get('/your/url').then(cb);
}
};
function cb(data) {
/// process data if you need
return data.accounts;
}
});
after that you can use it like this inside your controller:
.controller('myController', function (xhrService) {
$scope.allAccounts = [];
xhrService.getData()
.then(function (accounts) {
$scope.allAccounts = accounts;
return $scope.allAccounts;
})
.then(function () {
$scope.$watch('allAccounts', function (newValue) {
// do something
}
});
});
I think this is a good way to structure your code because you can reuse your service and you can add (or not) any watch you need (inside any controller)
And the most important, from the docs: https://docs.angularjs.org/api/ng/service/$q - "$q.then method returns a new promise which is resolved or rejected via the return value of the successCallback, errorCallback" - this is why each then callback needs a return statement.

How to encapsulate single and temporal events in a service?

I'm trying to encapsulate the events in a service in order to implement a mechanics to subscribe / unsubscribe the listeners when a controller's scope is destroyed. This because I have been using the rootScope.$on in the following way:
if(!$rootScope.$$listeners['event']) {
$rootScope.$on('event', function(ev, data){
// do some...
});
}
or
$scope.$on('$destroy', function(ev, data){
// unsubscribe the listener
});
So I just need one listener of this event, I need to delete the existing listener when the controller is no longer alive, because the function I registered earlier is still being triggered.
So I need to implement a $destroy event listener on my controller, to destroy the listener when the scope is destroyed, but I don't want to do that code each time I create an event.
That's why I want to create a service in where I'm going to encapsulate the events.
angular.module('core').factory('event', [
function() {
var service = {};
service.events = {};
service.on = function(scope, eventId, callback) {
scope.$on('$destroy', function(ev, other){
//unsubscribe
});
service.events[eventId] = callback;
// scope = null; I guess ?
};
service.emit = function(eventId, data){
if (service.events[eventId])
service.events[eventId](data);
else
return new Error('The event is not subscribed');
};
return service;
}
]);
This could be done using $rootScope instead of my own methods but encapsulating the $on and $emit of $rootScope, but at the end I'll have the same issue here.
So these are my questions:
Is a good practice to pass the scope ref value to a service?
What is the meaning of $$destroyed? when this is true means that angularJS has no internal references to the instance?
Should I do a scope = null in my service to let GC delete the object or does angularJS handle an explicit delete?
Is there a better way to do what I want?
What you are trying to accomplish is basically an event bus.
You have also described very well what is wrong with the current implementation.
A different way to approach the problem is to decorate the $rootScope with your bus (or any other event bus for that matter). Here is how:
app.config(function ($provide) {
$provide.decorator('$rootScope', ['$delegate', '$$bus', function ($delegate, $$bus) {
Object.defineProperty($delegate.constructor.prototype, '$bus', {
get: function () {
var self = this;
return {
subscribe: function () {
var sub = $$bus.subscribe.apply($$bus, arguments);
self.$on('$destroy',
function () {
console.log("unsubscribe!");
sub.unsubscribe();
});
},
publish: $$bus.publish
};
},
enumerable: false
});
return $delegate;
}]);
});
Considering the following $$bus implementation (kept basic for simplicity):
app.factory('$$bus', function () {
var api = {};
var events = {};
api.subscribe = function (event) {
if (!events.hasOwnProperty(event.name)) {
events[event.name] = [event];
} else {
events[event.name].push(event);
}
return {
unsubscribe: function () {
api.unsubscribe(event);
}
}
};
api.publish = function (eventName, data) {
if (events.hasOwnProperty(eventName)) {
console.log(eventName);
angular.forEach(events[eventName], function (subscriber) {
subscriber.callback.call(this, data);
});
}
};
api.unsubscribe = function (event) {
if (events.hasOwnProperty(event.name)) {
events[event.name].splice(events[event.name].indexOf(event), 1);
if (events[event.name].length == 0) {
delete events[event.name];
}
}
};
return api;
});
Now all you have to do is subscribe or publish events. The unsubscribe will take place automatically (when the $scope is destroyed):
$scope.$bus.subscribe({
name: 'test', callback: function (data) {
console.log(data);
}
});
And later on publish an event:
$scope.$bus.publish('test', {name: "publishing event!"});
An important point to make is that the events themselves are subscribed to each individual $scope and not on the $rootScope. That is how you "know" which $scope to release.
I think it answers your question. With that in mind, you can obviously make this mechanism much sophisticated (such as controller event listener released when a view routed, unsubscribe automatically only to certain events, etc.).
Good luck!
** This solution is taken form Here which uses a different bus framework (other then that it is the same).

Loading one dataset using data from another in Angular $watch

I am creating a messaging service that needs to do the following 1.) Load a messsage from our messages service, get the recipient's ids, and then load the recipients' info from a users service. I've tried both using the messages service callback, and also creating a watcher on the message object, without much success. The service works, but it doesn't assign the result to the $scope correctly. Here's the controller. All of the services are working correctly:
function MessageCtrl ($scope, $http, $location, $routeParams, Messages, Members) {
if ($routeParams.mid) { // Checks for the id of the message in the route. Otherwise, creates a new message.
$scope.mid = $routeParams.mid;
$scope.message = Messages.messages({mid: $scope.mid}).query();
$scope.$watch("message", function (newVal, oldVal, scope) {
if (newVal.data) {
$scope.recipients = Members.members({uids: newVal.data[0].uids}).query();
}
}, true);
} else {
$scope.create = true;
}
// Events
$scope.save = function () { };
$scope.preview = function () { };
$scope.send = function () { };
}
The correct way to use query is to perform the action in the callback that is passed in query function. In other words$scope.message should be assigned in the callback. Also you don't need a $watch. You can call the other service within the callback directly. But to keep it clean please use deferred
http://docs.angularjs.org/api/ngResource.$resource
http://docs.angularjs.org/api/ng.$q

Categories