self-updating angular factory with socket.io - angularjs

I'm trying to use an angular factory to provide a shared piece of state to my controllers. I also want this factory to automatically update its state by listening to a websocket. The goal is that I can write my data-fetching logic in one place, and then have the UI components update themselves automatically.
The problem I'm running into is that, while the factory is updating itself, the controllers never see the updates. Here's an example of what I'm trying to do:
app.factory('Socket', function () {
var Socket = io.connect();
return Socket;
});
app.factory('UpdateCounter', ['Socket', function (Socket) {
var counter = 0;
Socket.on('update', function () {
counter += 1;
});
return counter;
}]);
app.controller('MyController', ['$scope','UpdateCounter', function ($scope,UpdateCounter) {
$scope.counter = UpdateCounter;
...
}]);
MyController will see UpdateCounter = 0 and never see the changes.
I'm not surprised that this doesn't work, but I don't know why it doesn't work; nor do I know what I should be doing to get the behaviour I need.

You need to tell Angular that an update has occurred.
app.factory('UpdateCounter', ['Socket', '$rootScope', function (Socket, $rootScope) {
var counter = 0;
Socket.on('update', function () {
counter += 1;
$rootScope.$apply();
});
return counter;
}]);

There are a few ways you can do this. The first and simple is adding a watch so that the controller knows to watch the value for change.
app.controller('MyController', ['$scope','UpdateCounter', function ($scope,UpdateCounter) {
$scope.$watch(function(){
return UpdateCounter;
}, function(newVal){
$scope.counter = newVal;
})
}]);
The second and probably better way is to return an object that can be referenced
app.factory('UpdateCounter', ['Socket', function (Socket) {
var counter = { count: 0 };
Socket.on('update', function () {
counter.count += 1;
});
return counter;
}]);
app.controller('MyController', ['$scope','UpdateCounter', function ($scope,UpdateCounter) {
$scope.counter = UpdateCounter;
}]);
In your view, you can reference counter.count to get the count and it will be automatically updated.

Related

Angular JS: Using $scope.variable Across Controllers

So, I've got a counter ($scope.counter) that I increment on ng-click. The problem I'm having is that I want to increment it from two different controllers, keeping the counter value in sync.
controllerA.js
.controller('controllerA', controllerA);
function controllerA($scope) {
$scope.counter = 0;
function incrementCounter() {
$scope.counter = $scope.counter + 1;
}
...
controllerB.js
.controller('controllerB', controllerB);
function controllerB($scope) {
$scope.counter = 0;
function incrementCounter() {
$scope.counter = $scope.counter + 1;
}
...
When I call the incrementCounter() on 'controllerA' it updates the counter on 'controllerA', but not on 'controllerB' and vice versa.
Is there a proper way to keep these in sync, no matter which controller I call the incrementCounter() function from?
Any help is appreciated. Thanks in advance!
A service is a good way to share the counter variable. If you want to avoid watches to keep them in sync, make sure you define the variable as a property of an object on the service. You can set the value to null initially if you want to populate it with a $http callout as per your comment.
app.service('MyService', ['$http', function($http) {
var service = {
counter: {
value: null
},
incrementCounter: incrementCounter,
fetchTotal: fetchTotal
}
return service;
function incrementCounter(){
service.counter.value++;
}
function fetchTotal(url, p) {
$http.get(url, { params: p })
.then(function(response) {
service.counter.value = response.data.meta.total;
}, function(error) {
console.log("error occurred");
});
}
}]);
Then assign the counter object as a property on $scope in your controllers, and call the service to do the $http callout:
app.controller('Controller1', function ($scope, MyService) {
$scope.counter = MyService.counter;
$scope.incrementCounter = function(){
MyService.incrementCounter();
}
fetchTotal('/test', '');
});
Assigning the counter object as a property on $scope ensures that ensure two-way binding is intact in the view. You can use ng-if so you don't render anything until the $http call is complete and the counter is initialised.
<div ng-controller="Controller1">
<div ng-if="counter.value">
{{counter.value}}
<span ng-click="incrementCounter()">increment</span>
</div>
</div>
Fiddle with a mocked $httpBackend
There are two solutions.
using $rootScope instead of $scope.
Having a parent controller and define $scope.counter = 0 in it, then in your child controllers(A,B) that are used in this parent just update the $scope.counter value.
This is where services come in handy.
egghead.io has a nice video on this: https://egghead.io/lessons/angularjs-sharing-data-between-controllers
He talks a lot about ui-router as well, but it also shows how you can use a service.
UPDATE
If you want the value to update instantaniously in both controllers you can watch the service.
http://jsfiddle.net/jkrielaars/1swyy6re/2/
app.service('counterService', function() {
this.counter = 0;
this.addOne = function(){
this.counter++;
}
this.getCounter = function(){
return this.counter;
}
});
//Controllers
app.controller("controller1",function($scope, counterService){
$scope.counterService = counterService;
$scope.$watch('counterService.getCounter()', function (newVal) {
$scope.counter = newVal;
});
$scope.addOne = function(){
counterService.addOne();
}
})
app.controller("controller2",function($scope, counterService){
//same internals as controller 1
})
In your views you can now have both controllers that will update simultaniously
<div ng-controller="controller1">
counter value: {{counter}}
<button ng-click="addOne()">add 1</button>
</div>
<div ng-controller="controller2">
counter value: {{counter}}
<button ng-click="addOne()">add 1</button>
</div>
You also can use the broadcast of events to update the value in other controller. Here is how it can be done:
app.controller('MainCtrl', function($scope, $rootScope) {
$scope.name = 'World';
$scope.counter = 0;
$scope.incrementCounter = function() {
$scope.counter++;
$rootScope.$broadcast('incrementedInController1', $scope.counter);
}
$scope.$on('incrementedInController2', function(event, newValueCounter) {
$scope.counter = newValueCounter;
});
}).controller('MainCtrl2', function($scope, $rootScope) {
$scope.name2 = 'World2';
$scope.counter = 0;
$scope.incrementCounter = function() {
$scope.counter++;
$rootScope.$broadcast('incrementedInController2', $scope.counter);
}
$scope.$on('incrementedInController1', function(event, newValueCounter) {
$scope.counter = newValueCounter;
});
});
You can use $rootScope which can be accessed from all controllers but it's not recommended because of debuging issues.
.controller('controllerA', controllerA);
function controllerA($rootScope) {
$rootScope.counter = 0;
}
.controller('controllerB', controllerA);
function controllerB($rootScope) {
$rootScope.counter = 1;
}
if you are using something common between all controllers it's recommended to use Service or Factories instead
here is a sample when to use it :
http://viralpatel.net/blogs/angularjs-service-factory-tutorial/

AngularJS using service to pass data without $scope

I started using angularjs and the first examples i saw were ones that use this instead of $scope.
for example
app.controller('TabController',function(){
this.tab = 1;
});
I am trying to pass data between controllers. every example i see uses a service that is using $rootScope and then broadcast an event. the controller then uses the $scope.$on to listen to that event.
for example
app.service('mySharedService', function($rootScope) {
var sharedService = {};
sharedService.tab = 1;
sharedService.setTab= function(tab) {
this.tab = tab;
$rootScope.$broadcast('handleBroadcast');
};
return sharedService;
});
app.controller('TabController',['$scope','mySharedService',function($scope,mySharedService){
$scope.tab = 1;
$scope.$on('handleBroadcast', function() {
$scope.tab = sharedService.tab;
});
}]);
my question is how do i do this without using $scope but instead using this.
I took #Dieter Goetelen answer and changed it a bit
the result:
app = angular.module('app', []);
app.service('SharedService',function(){
this.counter = {value:0};
this.add = function (amount){
this.counter.value++;}
});
app.controller('TabController',['SharedService',function(SharedService){
this.sharedService = SharedService;
this.counter = SharedService.counter;
this.add = function(amount){
this.sharedService.add(amount);
}
}]);
here is the complete plunk

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 watch a variable defined in a service in angularjs

I am trying to watch changes on an json array defined in an angularj service, but when the change occures, the $watch function is not firing. My controller and service code goes as follows (plunker demo):
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope,cityService) {
//$scope.cities = [];
$scope.service = cityService;
cityService.initCities();
$scope.$watch('service.getCity()', function(newVal) {
$scope.cities = newVal;
console.log(newVal)
});
});
app.service('cityService', function($http) {
this.cities = [];
this.initCities = function() {
$http.get('data.js').success(function(data) {
this.cities = data;
});
};
this.getCity = function() {
return this.cities;
};
});
This is because the callback from get set this to window object. Keep the reference of the service in self.
See this plunker
http://plnkr.co/edit/CrgTWRBsg5wi7WOSZiRS?p=preview
I changed several things to make it work:
http://plnkr.co/edit/PDMaEvmx7hG1fKvAmR7R?p=preview
Function watch instead of variable
In the service, removed the keyword this because this has not the same context inside functions.
Return functions in service
Seems ok

angularJS: How to call child scope function in parent scope

How can call a method defined in child scope from its parent scope?
function ParentCntl() {
// I want to call the $scope.get here
}
function ChildCntl($scope) {
$scope.get = function() {
return "LOL";
}
}
http://jsfiddle.net/wUPdW/
You can use $broadcast from the parent to a child:
function ParentCntl($scope) {
$scope.msg = "";
$scope.get = function(){
$scope.$broadcast ('someEvent');
return $scope.msg;
}
}
function ChildCntl($scope) {
$scope.$on('someEvent', function(e) {
$scope.$parent.msg = $scope.get();
});
$scope.get = function(){
return "LOL";
}
}
Working fiddle: http://jsfiddle.net/wUPdW/2/
UPDATE: There is another version, less coupled and more testable:
function ParentCntl($scope) {
$scope.msg = "";
$scope.get = function(){
$scope.$broadcast ('someEvent');
return $scope.msg;
}
$scope.$on('pingBack', function(e,data) {
$scope.msg = data;
});
}
function ChildCntl($scope) {
$scope.$on('someEvent', function(e) {
$scope.$emit("pingBack", $scope.get());
});
$scope.get = function(){
return "LOL";
}
}
Fiddle: http://jsfiddle.net/uypo360u/
Let me suggest another solution:
var app = angular.module("myNoteApp", []);
app.controller("ParentCntl", function($scope) {
$scope.obj = {};
});
app.controller("ChildCntl", function($scope) {
$scope.obj.get = function() {
return "LOL";
};
});
Less code and using prototypical inheritance.
Plunk
Register the child's function on the parent when the child is initialising. I used "as" notation for clarity in the template.
TEMPLATE
<div ng-controller="ParentCntl as p">
<div ng-controller="ChildCntl as c" ng-init="p.init(c.get)"></div>
</div>
CONTROLLERS
...
function ParentCntl() {
var p = this;
p.init = function(fnToRegister) {
p.childGet = fnToRegister;
};
// call p.childGet when you want
}
function ChildCntl() {
var c = this;
c.get = function() {
return "LOL";
};
}
"But", you say, "ng-init isn't supposed to be used this way!". Well, yes, but
that documentation doesn't explain why not, and
I don't believe the documentation authors considered ALL possible use cases for it.
I say this is a good use for it. If you want to downvote me, please comment with reasons! :)
I like this approach because it keeps the components more modular. The only bindings are in the template, and means that
the child Controller doesn't have to know anything about which object to add its function to (as in #canttouchit's answer)
the parent control can be used with any other child control which has a get function
doesn't require broadcasting, which will get very ugly in a big app unless you tightly control the event namespace
This approach more closely approaches Tero's idea of modularising with directives (note that in his modularised example, contestants is passed from parent to "child" directive IN THE TEMPLATE).
Indeed another solution might be to consider implementing the ChildCntl as a directive and use the & binding to register the init method.
You can make child object.
var app = angular.module("myApp", []);
app.controller("ParentCntl", function($scope) {
$scope.child= {};
$scope.get = function(){
return $scope.child.get(); // you can call it. it will return 'LOL'
}
// or you can call it directly like $scope.child.get() once it loaded.
});
app.controller("ChildCntl", function($scope) {
$scope.obj.get = function() {
return "LOL";
};
});
Here child is proving destination of get method.

Resources