AngularJS : remove $rootScope from a service - angularjs

I have a service wrapped around WebSocket, I wanted to do it with promises and coupling requests with responses, here is what I came up with:
(function () {
var app = angular.module('mainModule');
app.service('$wsService', ['$q', '$rootScope', '$window', function($q, $rootScope, $window) {
var self = this;
// Keep all pending requests here until they get responses
var callbacks = {};
// Create a unique callback ID to map requests to responses
var currentCallbackId = 0;
var ws = new WebSocket("ws://127.0.0.1:9090");
this.webSocket = ws;
ws.onopen = function(){
$window.console.log("WS SERVICE: connected");
};
ws.onmessage = function(message) {
listener(JSON.parse(message.data));
};
var listener = function (messageObj) {
// If an object exists with callback_id in our callbacks object, resolve it
if(callbacks.hasOwnProperty(messageObj.Request.ID)) {
$rootScope.$apply(
callbacks[messageObj.Request.ID].cb.resolve(messageObj));
delete callbacks[messageObj.Request.ID];
}
};
// This creates a new callback ID for a request
var getCallbackId = function () {
currentCallbackId += 1;
if(currentCallbackId > 10000) {
currentCallbackId = 0;
}
return currentCallbackId;
};
//sends a request
var sendRequest = function (request, callback) {
var defer = $q.defer();
var callbackId = getCallbackId();
callbacks[callbackId] = {
time: new Date(),
cb:defer
};
request.ID = callbackId;
$window.console.log("WS SERVICE: sending " + JSON.stringify(request));
ws.send(JSON.stringify(request));
if(typeof callback === 'function') {
defer.promise.then(function(data) {
callback(null, data);
},
function(error) {
callback(error, null);
});
}
return defer.promise;
};
this.exampleCommand = function(someObject, callback){
var promise = sendRequest(someObject, callback);
return promise;
};
}]);
}());
And I use it in a controller like so:
(function () {
'use strict';
var app = angular.module('mainModule');
app.controller('someController', ['$scope', '$wsService', function ($scope, $wsService) {
$scope.doSomething = function(){
$wsService.exampleCommand(
{/*some data for the request here*/},
function(error, message){
//do something with the response
}
);
};
}]);
}());
After implementing this, I have been told that the service should not really operate on any kind of scope. So my question is - how would I go about removing the $rootScope from the service? I am not even sure if I should get rid of it, and if the conventions say I should, how to approach it. Thanks

I have been told that the service should not really operate on any kind of scope.
Who told you that? It's completely wrong.
Your service is receiving callbacks outside of a digest cycle from the websocket. To work with angular, those updates need to be applied inside a digest cycle - this is exactly what you're doing.
For reference, see the built in $http service. That wraps XMLHttpRequest analogously to how you're wrapping web sockets and it depends on $rootScope for exactly the functionality your code depends on $rootScope for.
Your code demonstrates the canonical use of $rootScope inside a service.

Related

My Service is returning data to the controller but I cannot seem to access it like I would expect.

(function () {
angular.module("app").controller('DashboardController', ['$q', 'dashboardService', function ($scope, $q,dashboardService) {
var DashboardController = this;
dashboardService.loadFromServer(DashboardController );
console.log("DashboardController ", DashboardController);
}])
})();
angular.module("app").service('dashboardService', ['$http', '$q', function ($http, $q) {
return {
loadFromServer: function (controller) {
var getDashboardEntries = $http.get('http://someUrl');
var getEmailData = $http.get('http://someOtherUrl');
var getSidebarData = $http.get('http://yetAnotherUrl');
return $q.all([getDashboardEntries, getSidebarData, getEmailData])
.then(function (results) {
controller.dashboardData = results[0].data;
controller.chartData = results[1].data;
controller.emailData = results[2].data;
});
},
};
}]);
1.The service returns the three bits of data and this is the results when logged using:
console.log("DashboardController ", DashboardController);
When I try to drill down on the data in this manner it logs "undefined"
console.log("DashboardController "DashboardController.dashboardData);
console.log("DashboardController "DashboardController.chartData);
console.log("DashboardController "DashboardController.emailData);
Do you realize that console.log is executed right after invoking loadFromServer before the server has chance to respond and promise resolves? The actual order is:
loadFromServer
console.log
promise success method - where you actually have your data
Change your controller's code to this:
dashboardService.loadFromServer(DashboardController ).then(function() {
console.log("DashboardController ", DashboardController);
});
What would be even better is to construct some object from parts of responses and assign it in the controller itself - not the service. In current implementation if you wanted to have another controller then service would assign response parts to same fields. I'd propose sth like this:
return $q.all([getDashboardEntries, getSidebarData, getEmailData])
.then(function (results) {
var data = {
dashboardData = results[0].data;
chartData = results[1].data;
emailData = results[2].data;
};
return data;
});
and then in controller:
dashboardService.loadFromServer().then(function(data) {
DashboardController.dashboardData = data.dashboardData;
DashboardController.chartData = data.chartData;
DashboardController.emailData = data.emailData;
});
In this solution the controller decides what to do with data, not the other way around.

AngularJS call scope function to 'refresh' scope model

I've been struggling with this for a few days now and can't seem to find a solution.
I have a simple listing in my view, fetched from MongoDB and I want it to refresh whenever I call the delete or update function.
Although it seems simple that I should be able to call a previously declared function within the same scope, it just doesn't work.
I tried setting the getDispositivos on a third service, but then the Injection gets all messed up. Declaring the function simply as var function () {...} but it doesn't work as well.
Any help is appreciated.
Here's my code:
var myApp = angular.module('appDispositivos', []);
/* My service */
myApp.service('dispositivosService',
['$http',
function($http) {
//...
this.getDispositivos = function(response) {
$http.get('http://localhost:3000/dispositivos').then(response);
}
//...
}
]
);
myApp.controller('dispositivoController',
['$scope', 'dispositivosService',
function($scope, dispositivosService) {
//This fetches data from Mongo...
$scope.getDispositivos = function () {
dispositivosService.getDispositivos(function(response) {
$scope.dispositivos = response.data;
});
};
//... and on page load it fills in the list
$scope.getDispositivos();
$scope.addDispositivo = function() {
dispositivosService.addDispositivo($scope.dispositivo);
$scope.getDispositivos(); //it should reload the view here...
$scope.dispositivo = '';
};
$scope.removeDispositivo = function (id) {
dispositivosService.removerDispositivo(id);
$scope.getDispositivos(); //... here
};
$scope.editDispositivo = function (id) {
dispositivosService.editDispositivo(id);
$scope.getDispositivos(); //... and here.
};
}
]
);
On service
this.getDispositivos = function(response) {
return $http.get('http://localhost:3000/dispositivos');
}
on controller
$scope.addDispositivo = function() {
dispositivosService.addDispositivo($scope.dispositivo).then(function(){
$scope.getDispositivos(); //it should reload the view here...
$scope.dispositivo = '';
});
};
None of the solutions worked. Later on I found that the GET request does execute, asynchronously however. This means that it loads the data into $scope before the POST request has finished, thus not including the just-included new data.
The solution is to synchronize the tasks (somewhat like in multithread programming), using the $q module, and to work with deferred objects and promises. So, on my service
.factory('dispositivosService',
['$http', '$q',
function($http, $q) {
return {
getDispositivos: function (id) {
getDef = $q.defer();
$http.get('http://myUrlAddress'+id)
.success(function(response){
getDef.resolve(response);
})
.error(function () {
getDef.reject('Failed GET request');
});
return getDef.promise;
}
}
}
}
])
On my controller:
$scope.addDispositivo = function() {
dispositivosService.addDispositivo($scope.dispositivo)
.then(function(){
dispositivosService.getDispositivos()
.then(function(dispositivos){
$scope.dispositivos = dispositivos;
$scope.dispositivo = '';
})
});
};
Being my 'response' object a $q.defer type object, then I can tell Angular that the response is asynchronous, and .then(---).then(---); logic completes the tasks, as the asynchronous requests finish.

How to integrate an AngularJS service with the digest loop

I'm trying to write an AngularJS library for Pusher (http://pusher.com) and have run into some problems with my understanding of the digest loop and how it works. I am writing what is essentially an Angular wrapper on top of the Pusher javascript library.
The problem I'm facing is that when a Pusher event is triggered and my app is subscribed to it, it receives the message but doesn't update the scope where the subscription was setup.
I have the following code at the moment:
angular.module('pusher-angular', [])
.provider('PusherService', function () {
var apiKey = '';
var initOptions = {};
this.setOptions = function (options) {
initOptions = options || initOptions;
return this;
};
this.setToken = function (token) {
apiKey = token || apiKey;
return this;
};
this.$get = ['$window',
function ($window) {
var pusher = new $window.Pusher(apiKey, initOptions);
return pusher;
}];
})
.factory('Pusher', ['$rootScope', '$q', 'PusherService', 'PusherEventsService',
function ($rootScope, $q, PusherService, PusherEventsService) {
var client = PusherService;
return {
subscribe: function (channelName) {
return client.subscribe(channelName);
}
}
}
]);
.controller('ItemListController', ['$scope', 'Pusher', function($scope, Pusher) {
$scope.items = [];
var channel = Pusher.subscribe('items')
channel.bind('new', function(item) {
console.log(item);
$scope.items.push(item);
})
}]);
and in another file that sets the app up:
angular.module('myApp', [
'pusher-angular'
]).
config(['PusherServiceProvider',
function(PusherServiceProvider) {
PusherServiceProvider
.setToken('API KEY')
.setOptions({});
}
]);
I've removed some of the code to make it more concise.
In the ItemListController the $scope.items variable doesn't update when a message is received from Pusher.
My question is how can I make it such that when a message is received from Pusher that it then triggers a digest such that the scope updates and the changes are reflected in the DOM?
Edit: I know that I can just wrap the subscribe callback in a $scope.$apply(), but I don't want to have to do that for every callback. Is there a way that I can integrate it with the service?
On the controller level:
Angular doesn't know about the channel.bind event, so you have to kick off the cycle yourself.
All you have to do is call $scope.$digest() after the $scope.items gets updated.
.controller('ItemListController', ['$scope', 'Pusher', function($scope, Pusher) {
$scope.items = [];
var channel = Pusher.subscribe('items')
channel.bind('new', function(item) {
console.log(item);
$scope.items.push(item);
$scope.$digest(); // <-- this should be all you need
})
Pusher Decorator Alternative:
.provider('PusherService', function () {
var apiKey = '';
var initOptions = {};
this.setOptions = function (options) {
initOptions = options || initOptions;
return this;
};
this.setToken = function (token) {
apiKey = token || apiKey;
return this;
};
this.$get = ['$window','$rootScope',
function ($window, $rootScope) {
var pusher = new $window.Pusher(apiKey, initOptions),
oldTrigger = pusher.trigger; // <-- save off the old pusher.trigger
pusher.trigger = function decoratedTrigger() {
// here we redefine the pusher.trigger to:
// 1. run the old trigger and save off the result
var result = oldTrigger.apply(pusher, arguments);
// 2. kick off the $digest cycle
$rootScope.$digest();
// 3. return the result from the the original pusher.trigger
return result;
};
return pusher;
}];
I found that I can do something like this and it works:
bind: function (eventName, callback) {
client.bind(eventName, function () {
callback.call(this, arguments[0]);
$rootScope.$apply();
});
},
channelBind: function (channelName, eventName, callback) {
var channel = client.channel(channelName);
channel.bind(eventName, function() {
callback.call(this, arguments[0]);
$rootScope.$apply();
})
},
I'm not really happy with this though, and it feels as though there must be something bigger than I'm missing that would make this better.

$interval making $http call, which promise is returned for chaining?

I have an angular service and a controller interacting. The service usings the $interval to poll the server. I know this returns a promise, however it uses $http to make an call to the server, which ALSO returns a promise and the chaining of the promises is not happening the way I would expect.
SERVICE
(function () {
'use strict';
var serviceId = "notificationService";
angular.module('app').factory(serviceId, ['helpersService', '$interval', '$http', function (helpersService, $interval, $http) {
var defaultOptions = {
url: undefined,
interval: 1000
};
var myIntervalPromise = undefined;
var displayedNotifications = [];
function onNotificationSuccess(response) {
//alert("in success");
displayedNotifications.push(response.data);
return response.data;
}
function onNotificationFailed(response) {
alert("in Failure");
throw response.data || 'An error occurred while attempting to process request';
}
function initializeNotificationService(configOptions) {
var passedOptions = $.extend({}, defaultOptions, configOptions);
if (passedOptions.url) {
myIntervalPromise = $interval(
function() {
//console.log(passedOptions.url);
//return helpersService.getAjaxPromise(passedOptions);
//promise.then(onNotificationSuccess, onNotificationFailed);
$http({
method: 'POST',
url: passedOptions.url
}).then(onNotificationSuccess, onNotificationFailed);
}, passedOptions.interval);
//alert("in initializeNotificationService");
return myIntervalPromise;
}
//return myIntervalPromise;
}
//$scope.$on('$destroy', function() {
// if (angular.isDefined(myIntervalPromise)) {
// $interval.cancel(myIntervalPromise);
// myIntervalPromise = undefined;
// }
//});
return {
// methods
initializeNotificationService: initializeNotificationService,
//properties
displayedNotifications : displayedNotifications
};
}]);
})();
CONTROLLER
(function () {
'use strict';
var controllerId = 'MessageCtrl';
//TODO: INVESTIGATE HAVING TO PASS $INTERVAL TO HERE TO DESTROY INTERVAL PROMISE.
//TODO: HAS TO BE A WAY TO MOVE THAT INTO THE SERVICE
angular.module('app').controller(controllerId, ['notificationService', '$scope', '$interval', function (notificationService, $scope, $interval) {
var vm = this;
// tied to UI element
vm.notifications = [];
vm.initialize = function () {
// initialize tyhe notification service here
var intervalPromise = notificationService.initializeNotificationService({ url: 'api/userProfile/getNotifications', interval: 5000 });
intervalPromise.then(
function (response) {
// NEVER GETS CALLED
var s = "";
//vm.notifications.push(response);
// alert("successful call");
},
function (response) {
var s = "";
// THIS GETS CALLED WHEN THE PROMISE IS DESTROYED
// response = canceled
//alert("failure to call");
},
function(iteration) {
console.log(notificationService.displayedNotifications);
// This gets called on every iteration of the $interval in the service
vm.notifications = notificationService.displayedNotifications;
}
);
// TODO: SEE COMMENT AT TOP OF CONTROLLER
$scope.$on('$destroy', function () {
if (angular.isDefined(intervalPromise)) {
$interval.cancel(intervalPromise);
intervalPromise = undefined;
}
});
};
vm.alertClicked = function (alert) {
alert.status = 'old';
};
// call to init the notification service here so when the controller is loaded the service is initialized
vm.initialize();
}]);
})();
The way this ends up flowing, and I'll do my best to show flow here
1) SERVICE - $interval makes the call with the $http BOTH OF THESE SEEM TO RETURN THEIR OWN PROMISES ACCORDING TO THE DOCS
2) CONTROLLER - intervalPromise's NOTIFY callack is called
3) SERVICE - onNotificationSuccess callback of $http is called
WHAT DOESN'T HAPPEN THAT I WOULD EXPECT
4) CONTROLLER - intervalPromise success callback is never called
Should the return response.data in the onNotificationSuccess handler within the service trigger the then chain in the Controller? It's aware that the promise is returned or seemingly cause the notify callback in the controller is called each time $interval executes, so I'm confused as to where the chain is broken.
IDEAL
$interval calls with $http, the promise from $http is passed up to the controller
then with each iteration new messages are added to the service on a successful call by $interval, then in the controller onsuccess I can check the property of the service and update the UI. Where am I losing the method chain?
I would recommend breaking the usage of $interval outside of service and use it directly in your controller.
The service being provided is the ability to get data from the server and the interval is the means in which to get the data, which is more indicative of the user interface's requirements as to how often the data is retrieved.
What you appear to be doing is to wrap the functionality of the $interval service which is causing a complication for you.
Note: after creating a quick plnkr the report progress event of $interval returns the iteration number (times called) and no other parameters.
Ended up with everything in the controller...
(function () {
'use strict';
var controllerId = 'NotificationCtrl';
angular.module('app').controller(controllerId, ['helpersService', '$scope', '$interval', function (helpersService, $scope, $interval) {
var vm = this;
var intervalPromise = undefined;
// tied to UI element
vm.notifications = [];
function onNotificationSuccess(response) {
//alert("in success");
vm.notifications.push.apply(vm.notifications, response.data);
return response.data;
}
function onNotificationFailed(response) {
//alert("in Failure");
throw response.data || 'An error occurred while attempting to process request';
}
vm.initialize = function () {
intervalPromise = $interval(
function () {
var promise = helpersService.getAjaxPromise({ url: 'api/userProfile/getNotifications' });
promise.then(onNotificationSuccess, onNotificationFailed);
}, 5000);
$scope.$on('$destroy', function () {
if (angular.isDefined(intervalPromise)) {
$interval.cancel(intervalPromise);
intervalPromise = undefined;
}
});
};
vm.alertClicked = function (alert) {
//alert.status = 'old';
};
// call to init the notification service here so when the controller is loaded the service is initialized
vm.initialize();
}]);
})();

Angular JS - communication service to directive

i cant get my head around what is the recommended way to communicate from a service to a custom directive. The custom directive is an interactive svg graphic, which on user interaction calls a method of an injected service to retrieve new data. This should happen in an asynchronous manner. I read here and there that events are in general not the recommend way to communicate in angularjs. Should I use a callback function? Or?
Thanks buddies
martin
You inject the service into the directive, and then the directive calls methods on the service passing in argument values as parameters.
To let a directive know that a service method has completed asynchronously, have the service method return a promise object.
http://jsfiddle.net/gGhtD/5/
var myApp = angular.module('myApp', []);
//myApp.directive('myDirective', function() {});
myApp.factory('myService', function ($q, $timeout) {
return {
doSomething: function (msg) {
var d = $q.defer();
$timeout(function () {
d.resolve("resolved: " + msg);
}, 1500);
return d.promise;
}
}
});
function MyCtrl($scope, myService) {
$scope.callService = function () {
$scope.sent = new Date();
$scope.msg = "";
$scope.timestamp = "";
myService.doSomething("some value")
.then(function (data) {
$scope.timestamp = new Date();
$scope.msg = data;
});
}
}

Resources