Alternatives to angularjs $watch to trigger ui change from service - angularjs

I'm wondering whether there is a different approach to using $watch in order to achieve the following.
Setup:
ControllerA depends on ServiceA.
ControllerB depends on ServiceB.
Current browser view is showing both controllers.
Scenario:
ControllerA is initiating a function on ServiceA, which in turn changes the value of propery X in ServiceB which should be reflected in the UI of ControllerB.
http://jsfiddle.net/zexscvax/2/
html:
<div>
<div ng-controller="ControllerA"></div>
<div ng-controller="ControllerB">Progress: {{progress}}</div>
</div>
js:
var myApp = angular.module('myApp', []);
myApp.factory('serviceA', ['$q', '$interval', 'serviceB', function ($q, $interval, serviceB) {
var service = {};
service.start = function () {
var progress = 0;
var deferred = $q.defer();
deferred.promise.then(null,null, notifyServiceB);
function notifyServiceB() {
serviceB.update(progress);
}
$interval(function() {
if (progress == 0.99) {
deferred.resolve();
} else {
progress += 0.01;
deferred.notify(progress);
}
}, 50, 100);
};
return service;
}]);
myApp.factory('serviceB', ['$rootScope', function ($rootScope) {
var service = {};
service.update = function (progress) {
console.log('update', progress);
service.progress = progress;
//$rootScope.$apply(); // <+ ERROR: $digest already in progress
};
return service;
}]);
myApp.controller('ControllerA', ['$scope', 'serviceA',
function ($scope, serviceA) {
serviceA.start();
}]);
myApp.controller('ControllerB', ['$scope', 'serviceB',
function ($scope, serviceB) {
$scope.progress = serviceB.progress;
/* this works but I'm not sure whether this is performing
$scope.$watch(function () { return serviceB.progress; },
function (value) {
$scope.progress = serviceB.progress;
}
);
*/
}]);
Without the $watch in ControllerB for the property X in ServiceB, the UI would not get updated. I've also tried injecting $rootScope in ServiceB in order to run an apply() but that wouldn't work.
I'm not entirely sure whether there's a better way to setup this scenario or whether $watch is fine. I'm a bit worried about performance issues as the value of property X changes almost every 50 ms (it's basically a visual timer counting down).
Thanks for your input.

If you don't use $watch, you can use $rootScope to broadcast, and on controller B, you can $on this event and handle the view update.

Related

$scope.$on changes not reflected in the view

I'm doing a broadcast and on listening to the broadcast i'm trying to updated a variable on the scope that I wanted to display on the view, but the changes are not being reflected in the view immediately, until I click on the UI. Anyone know what should be done at this point, I don't want to use $apply. Here, please find my code.
rApp.factory('pService', ['$http', '$rootScope', '$sanitize',
function ($http, $rootScope, $sanitize) {
var pService = {};
//Some other code
pService.Update=function(status)
{
if(status.LastItemId!=undefined)
{
pService.disItemId = status.LastItemId;
$rootScope.$broadcast('updated',pService.disItemId);
}
}
//Some other code
return pService;
});
rApp.controller('dController', ['$scope','$rootScope' 'pService' ,dController]);
function dController($scope,$rootScope, pService) {
$rootScope.$on('updated',function (event, data) {
$scope.lastItemId = data; // I want to display the lastItemId on UI
})
});
Ideally Services are used for sharing common methods in different controllers. It's good to return this. Also, if you need to return the value to controller, instead using events why don't you simply return the value from a service public method and access the value in controller. Also, it's the controllers' work to initiate or call services' method and update corresponding scopes. Firing event from a service to notify controller is similar to what firing events from one controller to other. Services are not made for that purpose. See my code for reference.
DEMO - http://plnkr.co/edit/Co7ka0sZKZgYk5Oz88Np?p=preview
JS:
var app = angular.module('myApp', []);
app.controller('myController', ['$scope', '$rootScope', 'pService', function ($scope, $rootScope, pService) {
$scope.name = 'softvar';
$scope.itemId = pService.Update({LastItemId: 4})
}]);
app.factory('pService', [ '$rootScope', function ( $rootScope) {
this.pService = {};
//Some other code
this.Update= function(status) {
if (status.LastItemId) {
this.pService.disItemId = status.LastItemId;
console.log(this.pService.disItemId)
return this.pService.disItemId;
}
}
//Some other code
return this;
}]);
HTML:
<body ng-app="myApp" ng-controller="myController">
<h1>ItemId is: {{itemId}}!</h1>
</body>
UPDATE:
DEMO - http://plnkr.co/edit/Co7ka0sZKZgYk5Oz88Np?p=preview
JS:
var app = angular.module('myApp', []);
app.controller('myController', ['$scope', '$rootScope', 'pService', function ($scope, $rootScope, pService) {
$scope.name = 'softvar';
$scope.$on('serviceUpdated', function (ev, data) {
$scope.itemId = data;
});
pService.Update({LastItemId: 4});
}]);
app.factory('pService', [ '$rootScope', function ( $rootScope) {
this.pService = {};
//Some other code
this.Update= function(status) {
if (status.LastItemId) {
this.pService.disItemId = status.LastItemId;
console.log(this.pService.disItemId)
$rootScope.$broadcast('serviceUpdated', this.pService.disItemId);
}
}
//Some other code
return this;
}]);
What triggers the event being sent, i.e. where does your service's update() method get called? You may need to use apply to trigger a digest cycle if it is called from outside angular. I see from the comment you are using SignalR, that will not create a digest cycle to update bindings in angular. Try wrapping your call in an apply like this:
$rootScope.$apply(function(scope) {
service.Update();
});
You also don't need to use $rootScope.on(), you can just use $scope.on(). Broadcasts on the root scope will go down to all child scopes. If the message isn't used elsewhere, you can use $rootScope.emit() which bubbles upward and won't go down through all your child scopes.

Angularjs data binding between controller and service

I'm not able to get the data binding between controller and service working.
I have a controller and a factory which makes an HTTP call. I would like to be able to call the factory method from other services and see the controller attributes get updated. I tried different options but none of them seem to be working. Any ideas would be greatly appreciated.
Please see the code here:
http://plnkr.co/edit/d3c16z?p=preview
Here is the javascript code.
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
});
app.controller('EventDetailCtrl', ['$http', 'EventDetailSvc', '$scope',
function ($http, EventDetailSvc, $scope) {
this.event = EventDetailSvc.event;
EventDetailSvc.getEvent();
console.log(self.event);
$scope.$watch(angular.bind(this, function () {
console.log('under watch');
console.log(this.event);
return this.event;
}), function (newVal, oldVal) {
console.log('under watch2');
console.log(newVal);
this.event = newVal;
});
}])
.factory('EventDetailSvc', ['$http', function ($http) {
var event = {};
var factory = {};
factory.getEvent = function() {
$http.get('http://ip.jsontest.com')
.then(function (response) {
this.event = response.data;
console.log('http successful');
console.log(this.event);
return this.event;
}, function (errResponse) {
console.error("error while retrieving event");
})
};
factory.event = event;
return factory;
}]);
It seems to me that you have nested the event object inside of a factory object. You should be returning event directly instead wrapping it with factory. As it stands now you would need to call EventDetailSvc.factory.event to access your object.

Angular.js $watch() not catching update from JS object member from Service

$watch() is not catching return sseHandler.result.cpuResult.timestamp after the first iteration. I'm not sure why, because I verified the datestamps are changing. Also, after the first iteration....if I click on the view repeatedly, the scope variables and view update with the new information...so it's like $watch does work...but only if I click on the view manually to make it work.
'use strict';
angular.module('monitorApp')
.controller('homeCtrl', function($scope, $location, $document) {
console.log("s");
});
angular.module('monitorApp')
.controller('cpuCtrl', ['$scope', 'sseHandler', function($scope, sseHandler) {
$scope.sseHandler = sseHandler;
$scope.avaiable = "";
$scope.apiTimeStamp = sseHandler.result.cpuResult.timestamp;
$scope.infoReceived = "";
$scope.last15 = "";
$scope.last5 = "";
$scope.lastMinute = "";
var cpuUpdate = function (result) {
$scope.available = result.cpuResult.avaiable;
$scope.apiTimeStamp = result.cpuResult.timestamp;
$scope.infoReceived = new Date();
$scope.last15 = result.cpuResult.metrics['15m'].data
$scope.last5 = result.cpuResult.metrics['5m'].data
$scope.lastMinute = result.cpuResult.metrics['1m'].data
}
$scope.$watch(function () {
console.log("being caught");
return sseHandler.result.cpuResult.timestamp},
function(){
console.log("sss");
cpuUpdate(sseHandler.result);
});
}]);
angular.module('monitorApp')
.controller('filesystemsCtrl', function($scope, $location, $document) {
console.log("s");
});
angular.module('monitorApp')
.controller('httpPortCtrl', function($scope, $location, $document) {
console.log("s");
});
angular.module('monitorApp')
.factory('sseHandler', function ($timeout) {
var source = new EventSource('/subscribe');
var sseHandler = {};
sseHandler.result = { "cpuResult" : { timestamp : '1'} };
source.addEventListener('message', function(e) {
result = JSON.parse(e.data);
event = Object.keys(result)[0];
switch(event) {
case "cpuResult":
sseHandler.result = result;
console.log(sseHandler.result.cpuResult.timestamp);
break;
}
});
return sseHandler;
});
The changes in sseHandler.result.cpuResult.timestamp happen otuside of the Angular context (in the asynchronously executed event-listener callback), so Angular does not know about the changes.
You need to manually trigger a $digest loop, by calling $rootScope.$apply():
.factory('sseHandler', function ($rootScope, $timeout) {
...
source.addEventListener('message', function (e) {
$rootScope.$apply(function () {
// Put all code in here, so Angular can also handle exceptions
// as if they happened inside the Angular context.
...
});
}
...
The reason your random clicking around the app made it work, is because you probably triggered some other action (e.g. changed a model, triggered and ngClick event etc) which in turn triggered a $digest cycle.
Your message event in the EventListener does not start a new digest cycle. In your sseHandler try:
$timeout(function () {sseHandler.result = result;});

How do I test a controller that watches for changes on an injected service?

I'm using a service to share data between controllers. If a value on the service changes, I want to update some data binding on my controllers. To do this, I'm using $scope.$watchCollection (because the value I'm watching is a simple array). I'm having trouble trying to figure out how to test this in Jasmine + Karma.
Here is a simple Controller + Service setup similar to what I'm doing in my app (but very simplified):
var app = angular.module('myApp', []);
// A Controller that depends on 'someService'
app.controller('MainCtrl', function($scope, someService) {
$scope.hasStuff = false;
// Watch someService.someValues for changes and do stuff.
$scope.$watchCollection(function(){
return someService.someValues;
}, function (){
if(someService.someValues.length > 0){
$scope.hasStuff = false;
} else {
$scope.hasStuff = true;
}
});
});
// A simple service potentially used in many controllers
app.factory('someService', function ($timeout, $q){
return {
someValues: []
};
});
And here is a test case that I've attempted (but does not work):
describe('Testing a controller and service', function() {
var $scope, ctrl;
var mockSomeService = {
someValues : []
};
beforeEach(function (){
module('myApp');
inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
ctrl = $controller('MainCtrl', {
$scope: $scope,
someService: mockSomeService
});
});
});
it('should update hasStuff when someService.someValues is changed', function (){
expect($scope.hasStuff).toEqual(false);
// Add an item to someService.someValues
someService.someValues.push(1);
//$apply the change to trigger the $watch.
$scope.$apply();
//assert
expect($scope.hasStuff).toEqual(true);
});
});
I guess my question is twofold:
How do I properly mock the service that is used in the controller?
How do I then test that the $watchCollection function is working properly?
Here is a plunkr for the above code. http://plnkr.co/edit/C1O2iO
Your test (or your code ) is not correct .
http://plnkr.co/edit/uhSdk6hvcHI2cWKBgj1y?p=preview
mockSomeService.someValues.push(1); // instead of someService.someValues.push(1);
and
if(someService.someValues.length > 0){
$scope.hasStuff = true;
} else {
$scope.hasStuff = false;
}
or your expectation makes no sense
I strongly encourage you to lint your javascript (jslint/eslint/jshint) to spot stupid errors like the first one.Or you'll have a painfull experience in writing javascript. jslint would have detected that the variable you were using didnt exist in the scope.

How to handle a global service for different views in angular?

Here is my scenario. I want to refresh my captcha if the form get error message. And I have multi views to use the captcha.
So I write a factory:
Services.factory 'Captcha', ['$rootScope', ($rootScope) ->
service = {}
service.new_captcha = () ->
console.log 'render cap'
$rootScope.captcha_src = "/captcha?action=captcha&i=#{+new Date}"
service
]
And then another factory where handle the $http process will trigger the code below
$http
.error (data) ->
service.signin_err_msg = data.error
Captcha.new_captcha()
$rootScope.$broadcast('new_captcha')
In the view controller, $scope value will listen to the broadcast and change the src value.
SignUpCtrl = App.controller 'SignUpCtrl', ($scope, UserService, $location, $rootScope) ->
$scope.UserService = UserService
$scope.$on 'new_captcha', (val) ->
$scope.captcha_src = $rootScope.captcha_src
$scope.captcha_src = $rootScope.captcha_src
This works. But I dont think this is a good way. I have to write the same code to listen the rootScope broadcast. Is there same method better?
As rtcherry says, you don't need to use $rootScope.
Please have a look at this Plunker: http://embed.plnkr.co/4ppfCi/preview
There is no need to use $rootScope or to broadcast any events. This would also work:
Captcha Service
Services.factory('Captcha', [function() {
var captchaSrc;
return {
get_captcha_src: function() {
if (!captchaSrc) {
this.refresh_captcha();
}
return captchaSrc;
},
refresh_captcha: function() {
captchaSrc = "/captcha?action=captcha&i=" + new Date();
}
}
}]);
HTTP Handler
$http.error(function(data) {
service.signin_err_msg = data.error;
Captcha.refresh_captcha();
});
Controller
var SignUpCtrl = App.controller('SignUpCtrl', function($scope, $location, UserService, Captcha) {
...
$scope.get_captcha_src = function() {
return Captcha.get_captcha_src();
}
// or this: $scope.get_captcha_src = Captcha.get_captcha_src;
});

Resources