I have a controller which manages I page of data and a service which makes an HTTP request every 30 seconds to get fresh data to show on the page. I'm trying to write this in an "Angular" way that is testable and leverages the service properly.
I can think of two basic approaches, and I'm guessing one (or maybe both) is wrong:
The controller stores the data in a $scope variable, and does a
setInterval
or$timeout
to call methods of the service to get new data and then update the variable.The service stores the data in it's own variables/property and periodically calls its self to get new data. And the controller somehow watches/listens to the service properties to know when to update the view.
For the purposes of this question, it may be helpful to consider a concrete example. If the HTTP request fails, I want to show the error to the view/user. So assume an errorMsg
variable that needs to live somewhere. Should it live in the controller? In which case, the service needs to return that value every time. Or should it live in the service, and the controller somehow watches for it.
I've tried the first approach, and it seems to result in a LOT of logic in the controller, mostly in then()
s which follow the service methods. My instinct is that #2 is the correct way to do it. But I'm a little unclear as to how the controller should listen/watch the service. Thanks in advance.
I have a controller which manages I page of data and a service which makes an HTTP request every 30 seconds to get fresh data to show on the page. I'm trying to write this in an "Angular" way that is testable and leverages the service properly.
I can think of two basic approaches, and I'm guessing one (or maybe both) is wrong:
The controller stores the data in a $scope variable, and does a
setInterval
or$timeout
to call methods of the service to get new data and then update the variable.The service stores the data in it's own variables/property and periodically calls its self to get new data. And the controller somehow watches/listens to the service properties to know when to update the view.
For the purposes of this question, it may be helpful to consider a concrete example. If the HTTP request fails, I want to show the error to the view/user. So assume an errorMsg
variable that needs to live somewhere. Should it live in the controller? In which case, the service needs to return that value every time. Or should it live in the service, and the controller somehow watches for it.
I've tried the first approach, and it seems to result in a LOT of logic in the controller, mostly in then()
s which follow the service methods. My instinct is that #2 is the correct way to do it. But I'm a little unclear as to how the controller should listen/watch the service. Thanks in advance.
- I agree that number 2 is the way to go, but it's a tricky problem. I don't really have help for you at this point, but I can at least offer you some semblance of validation. – S. Buda Commented Feb 4, 2015 at 23:45
6 Answers
Reset to default 9 +100Let's look at this from the controller's point of view:
The controller stores the data and queries the service
This is called pull. You are effectively creating a stream of server responses which you're polling in the controller
The service stores the data and the controller watches it
This is called push. You're effectively creating a stream of results and notifying the consumer of changes rather than it looking for them.
Those are both valid approaches for your issue. Pick the one that you find easier to reason about. Personally I agree that the second is cleaner since you don't have to be aware about it in the controller. This should get you a general idea:
function getServerState(onState){
return $http.get("/serverstate").then(function(res){
onState(res.data);// notify the watcher
}).catch(function(e){/*handle errors somehow*/})
.then(function(){
return getServerState(onState); // poll again when done call
});
}
Which you can consume like such:
getServerState(function(state){
$scope.foo = state; //since it's in a digest changes will reflect
});
Our last issue is that it leaks the scope since the code is not on the controller we have a callback registered to a scope that'll cease to exist. Since we can't use fun ES6 facilities for that yet - we'll have to provide a "I'm done" handle to the getServerState method return value, for example a .done
property which you'll call on a scope destroy event.
While Benjamin provided a very nice solution and I totally agree with his answer I would like to add, for the sake of pleteness, that there is an other alternative for your problem you maybe did not think of yet:
Using websockets.
With websockets your server can call the client directly so you do not need to poll for updates. Of course it depends on your scenario and server technologies whether this would be an option for you at all. But that's how you can create a true Push from the server to the client.
If you want to give it a try:
If you are working with .Net servers, then absolutely try SignalR. It is very nice to use and has a fall-back mechanism to long-polling.
There also exists this libray which I did not really use, so I cannot tell a lot: https://github./wilk/ng-websocket
Working with node.js you should have a look at Socket.IO
Regarding the implementation you can do both implementation but your second option might be cleaner also with web-sockets. That is the way I used to do it so far.
You can create simple Service (named ServerStateService) which uses $interval to get refresh data from the server every 30 seconds.
$interval(function() {
$http.get(YOUR_URL_PATH).success(function(responseData) {
$rootScope.$emit('server.state.change', responseData);
});
}, 30 * 1000);
When you get response from the server emit or broadcast the event from ServerStateService named 'server.state.change' with responseData from the server.
And finally handle it from the controller using angular event system.
$rootScope.$on('server.state.change', function(data) {
//Do somethig with data
$scope.serverData = data;
});
I think you should keep the logic in the controller (solution #1), because otherwise the service will be running infinitely even if no controller requires it which will cause some memory and network overhead for the entire application. And, I don't think the logic is too plicated for the controller anyway.
Also, passing scope functions to a service does not seem so elegant.
I'd do something like this:
- service:
factory('serverState', ['$http', function($http) { var update = function (){ return $http.get("serverUrl") } return { update:update }; }]);
- controller:
function updateSuccess(res){ $scope.state = res.state; } function updateFail(res){ $scope.state = "Not connected"; } $interval(function() { serverState.update().then(updateSuccess,updateFail); }, 30000);
While polling the server is a valid method of sharing state between the server and the client, there are other approaches that you can leverage for better performance and less overhead for the server. One of those is mentioned above, WebSockets. WebSockets, however, are fairly new and it would require a good amount of server-side work to support them.
One of the less known or mentioned methods for this is called Server Sent Events. It's part of the HTML5 standard and thus you will find all browsers implement it (except, of course, IE. Surprise!). SSE is a surprisingly simple way for the server to alert the client to state changes.
In my opinion the "Angular" way would be to write a directive.
The directive would contain an isolated scope/child controller. This would allow the two-way binding between controller and your html template file.
When the directive is loaded you would initiate a $timer object which periodically invokes an internal controller function that would in turn call an angular service (a proxy to your server side data.)
The response data from the service would then be used to update the $scope variable on the controller which is bound to your html template.
This also has the advantage of allowing you to cancel the timer when the directive is removed via the '$destroy' event on the directive scope.
The outline for a solution is below.
'use strict';
/*global angular,console,$:false*/
angular.module('testModule').
directive('testDirective', ['testService', function(testService) {
var testController = function($scope, testService) {
$scope.testData = {};
function serviceResponse(data) {
$scope.testData = data;
}
function serviceError(error) {
console.log(error);
}
var timeoutPromise = null;
function startTimer() {
timeoutPromise = $timeout(function() {
/*
* Call the service, which returns a promise that
* when resolved will follow the success or error path.
*/
testService.retrieveData().then(serviceResponse, serviceError);
}, 180000); // every 3 minutes
}
function cancelTimer() {
if (timeoutPromise !== null) {
$timeout.cancel(timeoutPromise);
}
}
$scope.$on('$destroy', function() {
cancelTimer();
});
// Start the timer
startTimer();
};
return {
restrict: 'EA',
scope: {},
templateUrl: 'testHTML.html',
controller: ['$scope', 'testService', testController],
link: function(scope, element, attrs) {
}
};
}
]);