AngularJS communicating event/change from service to controller - angularjs

I'm trying to communicate between two controllers. I'm aware that this can be done by raising an event and then using $rootScope.$broadcast, but is not advisable for large-scale applications. I have seen a lot of blog posts which recommend using a service to communicate, but have not be able to succeed to implement. My actual code is more complicated, but here is the gist:
HTML:
<body ng-app="app">
<div ng-controller="MainCtrl">
<span>Source: {{count}}</span>
<button ng-click="updateCount()">Increase Count</button>
</div>
<div ng-controller="ListCtrl">
Destination: {{updatedCount}}
</div>
</body>
JS:
(function () {
var app = angular.module("app", []);
app.factory("ShareDataSvc", function ($log) {
var currentCount = 0;
var set = function (val) {
$log.info('Setting service value to: ' + currentCount);
currentCount = val;
}
var get = function () {
return currentCount;
}
return {
set: set,
get: get
}
});
app.controller("MainCtrl", ['$scope', 'ShareDataSvc', function ($scope, ShareDataSvc) {
$scope.count = ShareDataSvc.get();
$scope.updateCount = function () {
$scope.count = $scope.count + 1;
ShareDataSvc.set($scope.count);
}
}]);
app.controller("ListCtrl", ["$scope", "ShareDataSvc", function ($scope, ShareDataSvc) {
$scope.updatedCount = ShareDataSvc.get();
// trigger alert if count updated
$scope.triggerAlert = function () {
alert('Count updated!');
}
}]);
}());
I'm trying to understand why the count in the destination is not updated by Angular even though it is data-bound. It is my understanding that when the count is updated in the SharedDataSvc, the updatedCount property will be recalculated.
What am I doing wrong here? The end result is to trigger the alert on every count update.

You run into the old copy by value problem. When you do
$scope.updatedCount = ShareDataSvc.get();
The updated count property is being set to the value that is returned from your get function, and therefore doesn't see future changes to the value being tracked in your service. You have two options to get around this. One is to add a watch to each of your controllers to monitor the value in the service. Not ideal. The second is to have object property in your service track the value and data bind that object to your scope. Something like (note I only show the interesting parts):
app.factory("ShareDataSvc", function ($log) {
var set = function (val) {
$log.info('Setting service value to: ' + currentCount);
this.data.count = val;
}
return {
data: {count: 0}
set: set
}
});
app.controller("MainCtrl", ['$scope', 'ShareDataSvc', function ($scope, ShareDataSvc) {
$scope.data = ShareDataSvc.data;
$scope.updateCount = function () {
ShareDataSve.data.count++; // and increment function in the service would be better
}
}]);
app.controller("ListCtrl", ["$scope", "ShareDataSvc", function ($scope, ShareDataSvc) {
$scope.data = ShareDataSvc.data;
}]);
Then in your HTML
<body ng-app="app">
<div ng-controller="MainCtrl">
<span>Source: {{data.count}}</span>
<button ng-click="updateCount()">Increase Count</button>
</div>
<div ng-controller="ListCtrl">
Destination: {{data.count}}
</div>
</body>

Related

How to invoke function of another controller from different controller

I am using angular js as my client side.
I want to invoke one function which is in another controller.
How can i achieve this?
var app = angular.module('MyApplication', []);
app.controller('Controller1', function($scope, $rootScope) {
$scope.function1 = function() {
console.log('Controller 1 clicked and Function 1 called');
}
//receive the fuction using $on MyFunction
$rootScope.$on("CallController1", function() {
console.log('Controller 2 clicked and Controller 1 called');
});
});
app.controller('Controller2', function($scope) {
$scope.function2 = function() {
console.log('Controller 2 clicked and Function 2 called');
$scope.$emit("CallController1"); //emit the fuction using MyFunction
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<div ng-app="MyApplication">
<div ng-controller="Controller1">
<button ng-click="function1()">function 1</button>
</div>
<br>
<div ng-controller="Controller2">
<button ng-click="function2()">function 2</button>
</div>
</div>
Though it can be done by making use of the rootScope but polluting the rootScope is a risky affair taking into consideration the digest and watch cycles that run whenever the state of the application changes at run time.Burdening it more is not recommended.
The best and cleanest way to share common code or functionality is to use service/factory and then use it in your respective controller.
For e.g. we can create a shared service like below
angular.factory('sharedList', function() {
var list = [];
return {
addItem: addItem,
getList: getList
};
function addItem(item) {
list.push(item);
}
function getList() {
return list;
}
});
and include this service in your controllers as
app.controller("MyCtrl", function($scope, sharedList) {
$scope.users = sharedList.getList();
});
app.controller("AnotherCtrl", function($scope, sharedList) {
$scope.firstUser = sharedList.getList()[0];
});
and in case if you want to add something then you can make the code changes likewise.
Hope it helps.

angularjs: not able to pass a variable value from controller to view

(function() {
angular
.module('myApp.home', [])
.factory('myService', function($http) {
return {
getInfo: function() {
// return something
}
};
})
.controller('HomeController', function($scope, $routeParams, myService) {
var my = this;
var sid;
myService.getInfo().success(function(data) {
my.id = data.id;
//sid= my.id;
});
//my.id = sid;
});
})();
I'm trying to access the variable id in my view. But I'm not able to do so. I'm getting an undefined variable error.
I even tried to declare a variable sid globally so that it can be accessed from everywhere and I tried assigning value to it as well but that effort also didn't bring up any results.
My services are working fine and they are returning data. I'm trying to use the id in my view but don't know where I messed it up.
Any help?
EDIT
And my HTML is like this:
<div data-id="my.id"></div>
If you want to use it in your view you have to bind it to a scope e.g. $scope.id - check this link for reference.
Below is your html, how it should be.
<div ng-app="app" ng-controller="HomeController as home">
<div data-id="home.id">{{home.id}}</div>
</div>
Below is how your controller part.
angular.module('app', [])
.controller('HomeController', function () {
this.id = "someValue";
});
http://jsfiddle.net/grdmLfxt/
use controllerAs : vm then you can access vm.id in your view.
HTML
<div ng-app="app" ng-controller="HomeController as vm">
<div data-id="vm.id">{{vm.id}}</div>
</div>
Controller
.controller('HomeController', function () {
var vm = this;
vm.id = "someValue";
});
for more info read this
small change in javascript code and html. try this.
Javascript
(function() {
angular
.module('myApp.home', [])
.factory('myService', function($http) {
return {
getInfo: function() {
// return something
}
};
})
.controller('HomeController', function($scope, $routeParams, myService) {
var my = this;
var sid;
myService.getInfo().success(function(data) {
$scope.id = data.id; // changed from my.id to $scope.id
//sid= my.id;
});
//my.id = sid;
});
})();
HTML
<div data-id="id"></div>

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/

Passing Data Between Controllers - Frustrated by lack of clarity in other answers

Like many before I want to pass data between controllers. Specifically I want the user to be able to keep track of creating another user. I looked at the following SO explanations and came away not getting the results I was looking for or with answers that I don't know how to implement:
How can I pass some data from one controller to another peer controller
angularjs - passing data between controllers
AngularJS: How can I pass variables between controllers?
I also don't think broadcast is the solution to the problem. I think this is something that should be done with a service or factory (either will do as long as it works since I don't really understand the difference between them right now)
My code:
SERVICE
angular.module('startupApp')
.service('vendorId', function () {
// AngularJS will instantiate a singleton by calling "new" on this function
var VendorId = [];
var Vendor = [];
return {
setVendorId: function(vendorId){
VendorId.push(vendorId);
},
getVendorId: function(){
return VendorId;
},
setVendor: function(vendor){
Vendor.push(vendor);
},
getVendor: function(){
return Vendor;
}
};
});
CONTROLLER 1
angular.module('startupApp')
.controller('controller1', function ($scope, Auth, $location, vendorId, $http) {
$scope.user = {};
vendorId.setVendor($scope.user);
vendorId.setVendorId($scope.user.id);
})
CONTROLLER 2
angular.module('startupApp')
.controller('controller2', function ($scope, Auth, $location, vendorId, $http) {
console.log(vendorId.getVendor());//[]
console.log(vendorId.getVendorId());//[]
})
These both end up as empty arrays. To be clear, there is page1.html that has a form that populates the $scope.user object in controller1. I want to be able to get this object on page2.html from the page2.html controller (controller2)
Your code should work, unless controller1 is not invoked before controller2.
run this code snippet and see by yourself.
angular.module("test", [])
.service("vendorId", function () {
var VendorId = [];
var Vendor = [];
return {
setVendorId: function(vendorId){
VendorId.push(vendorId);
},
getVendorId: function(){
return VendorId;
},
setVendor: function(vendor){
Vendor.push(vendor);
},
getVendor: function(){
return Vendor;
}
};
})
.controller("Controller1", function ($rootScope, vendorId) {
this.user = {id : 1};
this.save = function () {
vendorId.setVendor(this.user);
vendorId.setVendorId(this.user.id);
// simulates page change
$rootScope.saved = true;
}
})
.controller("Controller2", function (vendorId) {
this.user = vendorId.getVendor();
this.id = vendorId.getVendorId();
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div data-ng-app="test">
<div data-ng-if="!saved" data-ng-controller="Controller1 as scope1">
<input type="text" data-ng-model="scope1.user.name" />
<button data-ng-click="scope1.save()">Save</button>
</div>
<div data-ng-if="saved" data-ng-controller="Controller2 as scope2">
The user : {{scope2.user}} <br />
The user id : {{scope2.id}}
</div>
</div>

AngularJS : How to watch service variables?

I have a service, say:
factory('aService', ['$rootScope', '$resource', function ($rootScope, $resource) {
var service = {
foo: []
};
return service;
}]);
And I would like to use foo to control a list that is rendered in HTML:
<div ng-controller="FooCtrl">
<div ng-repeat="item in foo">{{ item }}</div>
</div>
In order for the controller to detect when aService.foo is updated I have cobbled together this pattern where I add aService to the controller's $scope and then use $scope.$watch():
function FooCtrl($scope, aService) {
$scope.aService = aService;
$scope.foo = aService.foo;
$scope.$watch('aService.foo', function (newVal, oldVal, scope) {
if(newVal) {
scope.foo = newVal;
}
});
}
This feels long-handed, and I've been repeating it in every controller that uses the service's variables. Is there a better way to accomplish watching shared variables?
You can always use the good old observer pattern if you want to avoid the tyranny and overhead of $watch.
In the service:
factory('aService', function() {
var observerCallbacks = [];
//register an observer
this.registerObserverCallback = function(callback){
observerCallbacks.push(callback);
};
//call this when you know 'foo' has been changed
var notifyObservers = function(){
angular.forEach(observerCallbacks, function(callback){
callback();
});
};
//example of when you may want to notify observers
this.foo = someNgResource.query().$then(function(){
notifyObservers();
});
});
And in the controller:
function FooCtrl($scope, aService){
var updateFoo = function(){
$scope.foo = aService.foo;
};
aService.registerObserverCallback(updateFoo);
//service now in control of updating foo
};
In a scenario like this, where multiple/unkown objects might be interested in changes, use $rootScope.$broadcast from the item being changed.
Rather than creating your own registry of listeners (which have to be cleaned up on various $destroys), you should be able to $broadcast from the service in question.
You must still code the $on handlers in each listener but the pattern is decoupled from multiple calls to $digest and thus avoids the risk of long-running watchers.
This way, also, listeners can come and go from the DOM and/or different child scopes without the service changing its behavior.
** update: examples **
Broadcasts would make the most sense in "global" services that could impact countless other things in your app. A good example is a User service where there are a number of events that could take place such as login, logout, update, idle, etc. I believe this is where broadcasts make the most sense because any scope can listen for an event, without even injecting the service, and it doesn't need to evaluate any expressions or cache results to inspect for changes. It just fires and forgets (so make sure it's a fire-and-forget notification, not something that requires action)
.factory('UserService', [ '$rootScope', function($rootScope) {
var service = <whatever you do for the object>
service.save = function(data) {
.. validate data and update model ..
// notify listeners and provide the data that changed [optional]
$rootScope.$broadcast('user:updated',data);
}
// alternatively, create a callback function and $broadcast from there if making an ajax call
return service;
}]);
The service above would broadcast a message to every scope when the save() function completed and the data was valid. Alternatively, if it's a $resource or an ajax submission, move the broadcast call into the callback so it fires when the server has responded. Broadcasts suit that pattern particularly well because every listener just waits for the event without the need to inspect the scope on every single $digest. The listener would look like:
.controller('UserCtrl', [ 'UserService', '$scope', function(UserService, $scope) {
var user = UserService.getUser();
// if you don't want to expose the actual object in your scope you could expose just the values, or derive a value for your purposes
$scope.name = user.firstname + ' ' +user.lastname;
$scope.$on('user:updated', function(event,data) {
// you could inspect the data to see if what you care about changed, or just update your own scope
$scope.name = user.firstname + ' ' + user.lastname;
});
// different event names let you group your code and logic by what happened
$scope.$on('user:logout', function(event,data) {
.. do something differently entirely ..
});
}]);
One of the benefits of this is the elimination of multiple watches. If you were combining fields or deriving values like the example above, you'd have to watch both the firstname and lastname properties. Watching the getUser() function would only work if the user object was replaced on updates, it would not fire if the user object merely had its properties updated. In which case you'd have to do a deep watch and that is more intensive.
$broadcast sends the message from the scope it's called on down into any child scopes. So calling it from $rootScope will fire on every scope. If you were to $broadcast from your controller's scope, for example, it would fire only in the scopes that inherit from your controller scope. $emit goes the opposite direction and behaves similarly to a DOM event in that it bubbles up the scope chain.
Keep in mind that there are scenarios where $broadcast makes a lot of sense, and there are scenarios where $watch is a better option - especially if in an isolate scope with a very specific watch expression.
I'm using similar approach as #dtheodot but using angular promise instead of passing callbacks
app.service('myService', function($q) {
var self = this,
defer = $q.defer();
this.foo = 0;
this.observeFoo = function() {
return defer.promise;
}
this.setFoo = function(foo) {
self.foo = foo;
defer.notify(self.foo);
}
})
Then wherever just use myService.setFoo(foo) method to update foo on service. In your controller you can use it as:
myService.observeFoo().then(null, null, function(foo){
$scope.foo = foo;
})
First two arguments of then are success and error callbacks, third one is notify callback.
Reference for $q.
Without watches or observer callbacks (http://jsfiddle.net/zymotik/853wvv7s/):
JavaScript:
angular.module("Demo", [])
.factory("DemoService", function($timeout) {
function DemoService() {
var self = this;
self.name = "Demo Service";
self.count = 0;
self.counter = function(){
self.count++;
$timeout(self.counter, 1000);
}
self.addOneHundred = function(){
self.count+=100;
}
self.counter();
}
return new DemoService();
})
.controller("DemoController", function($scope, DemoService) {
$scope.service = DemoService;
$scope.minusOneHundred = function() {
DemoService.count -= 100;
}
});
HTML
<div ng-app="Demo" ng-controller="DemoController">
<div>
<h4>{{service.name}}</h4>
<p>Count: {{service.count}}</p>
</div>
</div>
This JavaScript works as we are passing an object back from the service rather than a value. When a JavaScript object is returned from a service, Angular adds watches to all of its properties.
Also note that I am using 'var self = this' as I need to keep a reference to the original object when the $timeout executes, otherwise 'this' will refer to the window object.
I stumbled upon this question looking for something similar, but I think it deserves a thorough explanation of what's going on, as well as some additional solutions.
When an angular expression such as the one you used is present in the HTML, Angular automatically sets up a $watch for $scope.foo, and will update the HTML whenever $scope.foo changes.
<div ng-controller="FooCtrl">
<div ng-repeat="item in foo">{{ item }}</div>
</div>
The unsaid issue here is that one of two things are affecting aService.foo such that the changes are undetected. These two possibilities are:
aService.foo is getting set to a new array each time, causing the reference to it to be outdated.
aService.foo is being updated in such a way that a $digest cycle is not triggered on the update.
Problem 1: Outdated References
Considering the first possibility, assuming a $digest is being applied, if aService.foo was always the same array, the automatically set $watch would detect the changes, as shown in the code snippet below.
Solution 1-a: Make sure the array or object is the same object on each update
angular.module('myApp', [])
.factory('aService', [
'$interval',
function($interval) {
var service = {
foo: []
};
// Create a new array on each update, appending the previous items and
// adding one new item each time
$interval(function() {
if (service.foo.length < 10) {
var newArray = []
Array.prototype.push.apply(newArray, service.foo);
newArray.push(Math.random());
service.foo = newArray;
}
}, 1000);
return service;
}
])
.factory('aService2', [
'$interval',
function($interval) {
var service = {
foo: []
};
// Keep the same array, just add new items on each update
$interval(function() {
if (service.foo.length < 10) {
service.foo.push(Math.random());
}
}, 1000);
return service;
}
])
.controller('FooCtrl', [
'$scope',
'aService',
'aService2',
function FooCtrl($scope, aService, aService2) {
$scope.foo = aService.foo;
$scope.foo2 = aService2.foo;
}
]);
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-app="myApp">
<div ng-controller="FooCtrl">
<h1>Array changes on each update</h1>
<div ng-repeat="item in foo">{{ item }}</div>
<h1>Array is the same on each udpate</h1>
<div ng-repeat="item in foo2">{{ item }}</div>
</div>
</body>
</html>
As you can see, the ng-repeat supposedly attached to aService.foo does not update when aService.foo changes, but the ng-repeat attached to aService2.foo does. This is because our reference to aService.foo is outdated, but our reference to aService2.foo is not. We created a reference to the initial array with $scope.foo = aService.foo;, which was then discarded by the service on it's next update, meaning $scope.foo no longer referenced the array we wanted anymore.
However, while there are several ways to make sure the initial reference is kept in tact, sometimes it may be necessary to change the object or array. Or what if the service property references a primitive like a String or Number? In those cases, we cannot simply rely on a reference. So what can we do?
Several of the answers given previously already give some solutions to that problem. However, I am personally in favor of using the simple method suggested by Jin and thetallweeks in the comments:
just reference aService.foo in the html markup
Solution 1-b: Attach the service to the scope, and reference {service}.{property} in the HTML.
Meaning, just do this:
HTML:
<div ng-controller="FooCtrl">
<div ng-repeat="item in aService.foo">{{ item }}</div>
</div>
JS:
function FooCtrl($scope, aService) {
$scope.aService = aService;
}
angular.module('myApp', [])
.factory('aService', [
'$interval',
function($interval) {
var service = {
foo: []
};
// Create a new array on each update, appending the previous items and
// adding one new item each time
$interval(function() {
if (service.foo.length < 10) {
var newArray = []
Array.prototype.push.apply(newArray, service.foo);
newArray.push(Math.random());
service.foo = newArray;
}
}, 1000);
return service;
}
])
.controller('FooCtrl', [
'$scope',
'aService',
function FooCtrl($scope, aService) {
$scope.aService = aService;
}
]);
<!DOCTYPE html>
<html>
<head>
<script data-require="angular.js#1.4.7" data-semver="1.4.7" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-app="myApp">
<div ng-controller="FooCtrl">
<h1>Array changes on each update</h1>
<div ng-repeat="item in aService.foo">{{ item }}</div>
</div>
</body>
</html>
That way, the $watch will resolve aService.foo on each $digest, which will get the correctly updated value.
This is kind of what you were trying to do with your workaround, but in a much less round about way. You added an unnecessary $watch in the controller which explicitly puts foo on the $scope whenever it changes. You don't need that extra $watch when you attach aService instead of aService.foo to the $scope, and bind explicitly to aService.foo in the markup.
Now that's all well and good assuming a $digest cycle is being applied. In my examples above, I used Angular's $interval service to update the arrays, which automatically kicks off a $digest loop after each update. But what if the service variables (for whatever reason) aren't getting updated inside the "Angular world". In other words, we dont have a $digest cycle being activated automatically whenever the service property changes?
Problem 2: Missing $digest
Many of the solutions here will solve this issue, but I agree with Code Whisperer:
The reason why we're using a framework like Angular is to not cook up our own observer patterns
Therefore, I would prefer to continue to use the aService.foo reference in the HTML markup as shown in the second example above, and not have to register an additional callback within the Controller.
Solution 2: Use a setter and getter with $rootScope.$apply()
I was surprised no one has yet suggested the use of a setter and getter. This capability was introduced in ECMAScript5, and has thus been around for years now. Of course, that means if, for whatever reason, you need to support really old browsers, then this method will not work, but I feel like getters and setters are vastly underused in JavaScript. In this particular case, they could be quite useful:
factory('aService', [
'$rootScope',
function($rootScope) {
var realFoo = [];
var service = {
set foo(a) {
realFoo = a;
$rootScope.$apply();
},
get foo() {
return realFoo;
}
};
// ...
}
angular.module('myApp', [])
.factory('aService', [
'$rootScope',
function($rootScope) {
var realFoo = [];
var service = {
set foo(a) {
realFoo = a;
$rootScope.$apply();
},
get foo() {
return realFoo;
}
};
// Create a new array on each update, appending the previous items and
// adding one new item each time
setInterval(function() {
if (service.foo.length < 10) {
var newArray = [];
Array.prototype.push.apply(newArray, service.foo);
newArray.push(Math.random());
service.foo = newArray;
}
}, 1000);
return service;
}
])
.controller('FooCtrl', [
'$scope',
'aService',
function FooCtrl($scope, aService) {
$scope.aService = aService;
}
]);
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-app="myApp">
<div ng-controller="FooCtrl">
<h1>Using a Getter/Setter</h1>
<div ng-repeat="item in aService.foo">{{ item }}</div>
</div>
</body>
</html>
Here I added a 'private' variable in the service function: realFoo. This get's updated and retrieved using the get foo() and set foo() functions respectively on the service object.
Note the use of $rootScope.$apply() in the set function. This ensures that Angular will be aware of any changes to service.foo. If you get 'inprog' errors see this useful reference page, or if you use Angular >= 1.3 you can just use $rootScope.$applyAsync().
Also be wary of this if aService.foo is being updated very frequently, since that could significantly impact performance. If performance would be an issue, you could set up an observer pattern similar to the other answers here using the setter.
As far as I can tell, you dont have to do something as elaborate as that. You have already assigned foo from the service to your scope and since foo is an array ( and in turn an object it is assigned by reference! ). So, all that you need to do is something like this :
function FooCtrl($scope, aService) {
$scope.foo = aService.foo;
}
If some, other variable in this same Ctrl is dependant on foo changing then yes, you would need a watch to observe foo and make changes to that variable. But as long as it is a simple reference watching is unnecessary. Hope this helps.
You can insert the service in $rootScope and watch:
myApp.run(function($rootScope, aService){
$rootScope.aService = aService;
$rootScope.$watch('aService', function(){
alert('Watch');
}, true);
});
In your controller:
myApp.controller('main', function($scope){
$scope.aService.foo = 'change';
});
Other option is to use a external library like: https://github.com/melanke/Watch.JS
Works with: IE 9+, FF 4+, SF 5+, WebKit, CH 7+, OP 12+, BESEN, Node.JS , Rhino 1.7+
You can observe the changes of one, many or all object attributes.
Example:
var ex3 = {
attr1: 0,
attr2: "initial value of attr2",
attr3: ["a", 3, null]
};
watch(ex3, function(){
alert("some attribute of ex3 changes!");
});
ex3.attr3.push("new value");​
You can watch the changes within the factory itself and then broadcast a change
angular.module('MyApp').factory('aFactory', function ($rootScope) {
// Define your factory content
var result = {
'key': value
};
// add a listener on a key
$rootScope.$watch(function () {
return result.key;
}, function (newValue, oldValue, scope) {
// This is called after the key "key" has changed, a good idea is to broadcast a message that key has changed
$rootScope.$broadcast('aFactory:keyChanged', newValue);
}, true);
return result;
});
Then in your controller:
angular.module('MyApp').controller('aController', ['$rootScope', function ($rootScope) {
$rootScope.$on('aFactory:keyChanged', function currentCityChanged(event, value) {
// do something
});
}]);
In this manner you put all the related factory code within its description then you can only rely on the broadcast from outside
==UPDATED==
Very simple now in $watch.
Pen here.
HTML:
<div class="container" data-ng-app="app">
<div class="well" data-ng-controller="FooCtrl">
<p><strong>FooController</strong></p>
<div class="row">
<div class="col-sm-6">
<p>Send one item</p>
<p>Send two items</p>
<p>Send three items</p>
</div>
<div class="col-sm-6">
<p>Send name: Sheldon</p>
<p>Send name: Leonard</p>
<p>Send name: Penny</p>
</div>
</div>
</div>
<div class="well" data-ng-controller="BarCtrl">
<p><strong>BarController</strong></p>
<p ng-if="name">Name is: {{ name }}</p>
<div ng-repeat="item in items">{{ item.name }}</div>
</div>
</div>
JavaScript:
var app = angular.module('app', []);
app.factory('PostmanService', function() {
var Postman = {};
Postman.set = function(key, val) {
Postman[key] = val;
};
Postman.get = function(key) {
return Postman[key];
};
Postman.watch = function($scope, key, onChange) {
return $scope.$watch(
// This function returns the value being watched. It is called for each turn of the $digest loop
function() {
return Postman.get(key);
},
// This is the change listener, called when the value returned from the above function changes
function(newValue, oldValue) {
if (newValue !== oldValue) {
// Only update if the value changed
$scope[key] = newValue;
// Run onChange if it is function
if (angular.isFunction(onChange)) {
onChange(newValue, oldValue);
}
}
}
);
};
return Postman;
});
app.controller('FooCtrl', ['$scope', 'PostmanService', function($scope, PostmanService) {
$scope.setItems = function(items) {
PostmanService.set('items', items);
};
$scope.setName = function(name) {
PostmanService.set('name', name);
};
}]);
app.controller('BarCtrl', ['$scope', 'PostmanService', function($scope, PostmanService) {
$scope.items = [];
$scope.name = '';
PostmanService.watch($scope, 'items');
PostmanService.watch($scope, 'name', function(newVal, oldVal) {
alert('Hi, ' + newVal + '!');
});
}]);
Building on dtheodor's answer you could use something similar to the below to ensure that you don't forget to unregister the callback... Some may object to passing the $scope to a service though.
factory('aService', function() {
var observerCallbacks = [];
/**
* Registers a function that will be called when
* any modifications are made.
*
* For convenience the callback is called immediately after registering
* which can be prevented with `preventImmediate` param.
*
* Will also automatically unregister the callback upon scope destory.
*/
this.registerObserver = function($scope, cb, preventImmediate){
observerCallbacks.push(cb);
if (preventImmediate !== true) {
cb();
}
$scope.$on('$destroy', function () {
observerCallbacks.remove(cb);
});
};
function notifyObservers() {
observerCallbacks.forEach(function (cb) {
cb();
});
};
this.foo = someNgResource.query().$then(function(){
notifyObservers();
});
});
Array.remove is an extension method which looks like this:
/**
* Removes the given item the current array.
*
* #param {Object} item The item to remove.
* #return {Boolean} True if the item is removed.
*/
Array.prototype.remove = function (item /*, thisp */) {
var idx = this.indexOf(item);
if (idx > -1) {
this.splice(idx, 1);
return true;
}
return false;
};
Here's my generic approach.
mainApp.service('aService',[function(){
var self = this;
var callbacks = {};
this.foo = '';
this.watch = function(variable, callback) {
if (typeof(self[variable]) !== 'undefined') {
if (!callbacks[variable]) {
callbacks[variable] = [];
}
callbacks[variable].push(callback);
}
}
this.notifyWatchersOn = function(variable) {
if (!self[variable]) return;
if (!callbacks[variable]) return;
angular.forEach(callbacks[variable], function(callback, key){
callback(self[variable]);
});
}
this.changeFoo = function(newValue) {
self.foo = newValue;
self.notifyWatchersOn('foo');
}
}]);
In Your Controller
function FooCtrl($scope, aService) {
$scope.foo;
$scope._initWatchers = function() {
aService.watch('foo', $scope._onFooChange);
}
$scope._onFooChange = function(newValue) {
$scope.foo = newValue;
}
$scope._initWatchers();
}
FooCtrl.$inject = ['$scope', 'aService'];
For those like me just looking for a simple solution, this does almost exactly what you expect from using normal $watch in controllers.
The only difference is, that it evaluates the string in it's javascript context and not on a specific scope. You'll have to inject $rootScope into your service, although it is only used to hook into the digest cycles properly.
function watch(target, callback, deep) {
$rootScope.$watch(function () {return eval(target);}, callback, deep);
};
while facing a very similar issue I watched a function in scope and had the function return the service variable. I have created a js fiddle. you can find the code below.
var myApp = angular.module("myApp",[]);
myApp.factory("randomService", function($timeout){
var retValue = {};
var data = 0;
retValue.startService = function(){
updateData();
}
retValue.getData = function(){
return data;
}
function updateData(){
$timeout(function(){
data = Math.floor(Math.random() * 100);
updateData()
}, 500);
}
return retValue;
});
myApp.controller("myController", function($scope, randomService){
$scope.data = 0;
$scope.dataUpdated = 0;
$scope.watchCalled = 0;
randomService.startService();
$scope.getRandomData = function(){
return randomService.getData();
}
$scope.$watch("getRandomData()", function(newValue, oldValue){
if(oldValue != newValue){
$scope.data = newValue;
$scope.dataUpdated++;
}
$scope.watchCalled++;
});
});
I came to this question but it turned out my problem was that I was using setInterval when I should have been using the angular $interval provider. This is also the case for setTimeout (use $timeout instead). I know it's not the answer to the OP's question, but it might help some, as it helped me.
I have found a really great solution on the other thread with a similar problem but totally different approach. Source: AngularJS : $watch within directive is not working when $rootScope value is changed
Basically the solution there tells NOT TO use $watch as it is very heavy solution. Instead they propose to use $emit and $on.
My problem was to watch a variable in my service and react in directive. And with the above method it very easy!
My module/service example:
angular.module('xxx').factory('example', function ($rootScope) {
var user;
return {
setUser: function (aUser) {
user = aUser;
$rootScope.$emit('user:change');
},
getUser: function () {
return (user) ? user : false;
},
...
};
});
So basically I watch my user - whenever it is set to new value I $emit a user:change status.
Now in my case, in the directive I used:
angular.module('xxx').directive('directive', function (Auth, $rootScope) {
return {
...
link: function (scope, element, attrs) {
...
$rootScope.$on('user:change', update);
}
};
});
Now in the directive I listen on the $rootScope and on the given change - I react respectively. Very easy and elegant!
// service: (nothing special here)
myApp.service('myService', function() {
return { someVariable:'abc123' };
});
// ctrl:
myApp.controller('MyCtrl', function($scope, myService) {
$scope.someVariable = myService.someVariable;
// watch the service and update this ctrl...
$scope.$watch(function(){
return myService.someVariable;
}, function(newValue){
$scope.someVariable = newValue;
});
});
A wee bit ugly, but I've added registration of scope variables to my service for a toggle:
myApp.service('myService', function() {
var self = this;
self.value = false;
self.c2 = function(){};
self.callback = function(){
self.value = !self.value;
self.c2();
};
self.on = function(){
return self.value;
};
self.register = function(obj, key){
self.c2 = function(){
obj[key] = self.value;
obj.$apply();
}
};
return this;
});
And then in the controller:
function MyCtrl($scope, myService) {
$scope.name = 'Superhero';
$scope.myVar = false;
myService.register($scope, 'myVar');
}
Have a look at this plunker:: this is the simplest example i could think of
http://jsfiddle.net/HEdJF/
<div ng-app="myApp">
<div ng-controller="FirstCtrl">
<input type="text" ng-model="Data.FirstName"><!-- Input entered here -->
<br>Input is : <strong>{{Data.FirstName}}</strong><!-- Successfully updates here -->
</div>
<hr>
<div ng-controller="SecondCtrl">
Input should also be here: {{Data.FirstName}}<!-- How do I automatically updated it here? -->
</div>
</div>
// declare the app with no dependencies
var myApp = angular.module('myApp', []);
myApp.factory('Data', function(){
return { FirstName: '' };
});
myApp.controller('FirstCtrl', function( $scope, Data ){
$scope.Data = Data;
});
myApp.controller('SecondCtrl', function( $scope, Data ){
$scope.Data = Data;
});
I've seen some terrible observer patterns here that cause memory leaks on large applications.
I might be a little late but it's as simple as this.
The watch function watches for reference changes (primitive types) if you want to watch something like array push simply use:
someArray.push(someObj); someArray = someArray.splice(0);
This will update the reference and update the watch from anywhere. Including a services getter method.
Anything that's a primitive will be updated automatically.
I am late to the part but I found a nicer way to do this than the answer posted above. Instead of assigning a variable to hold the value of the service variable, I created a function attached to the scope, that returns the service variable.
controller
$scope.foo = function(){
return aService.foo;
}
I think this will do what you want. My controller keeps checking the value of my service with this implementation. Honestly, this is much simpler than the selected answer.
I have written two simple utility services that help me track service properties changes.
If you want to skip the long explanation, you can go strait to jsfiddle
WatchObj
mod.service('WatchObj', ['$rootScope', WatchObjService]);
function WatchObjService($rootScope) {
// returns watch function
// obj: the object to watch for
// fields: the array of fields to watch
// target: where to assign changes (usually it's $scope or controller instance)
// $scope: optional, if not provided $rootScope is use
return function watch_obj(obj, fields, target, $scope) {
$scope = $scope || $rootScope;
//initialize watches and create an array of "unwatch functions"
var watched = fields.map(function(field) {
return $scope.$watch(
function() {
return obj[field];
},
function(new_val) {
target[field] = new_val;
}
);
});
//unregister function will unregister all our watches
var unregister = function unregister_watch_obj() {
watched.map(function(unregister) {
unregister();
});
};
//automatically unregister when scope is destroyed
$scope.$on('$destroy', unregister);
return unregister;
};
}
This service is used in the controller in the following way:
Suppose you have a service "testService" with the properties 'prop1', 'prop2', 'prop3'. You want to watch and assign to scope 'prop1' and 'prop2'. With the watch service it will look like that:
app.controller('TestWatch', ['$scope', 'TestService', 'WatchObj', TestWatchCtrl]);
function TestWatchCtrl($scope, testService, watch) {
$scope.prop1 = testService.prop1;
$scope.prop2 = testService.prop2;
$scope.prop3 = testService.prop3;
watch(testService, ['prop1', 'prop2'], $scope, $scope);
}
apply
Watch obj is great, but it is not enough if you have asynchronous code in your service. For that case, I use a second utility which looks like that:
mod.service('apply', ['$timeout', ApplyService]);
function ApplyService($timeout) {
return function apply() {
$timeout(function() {});
};
}
I would trigger it in the end of my async code to trigger the $digest loop.
Like that:
app.service('TestService', ['apply', TestService]);
function TestService(apply) {
this.apply = apply;
}
TestService.prototype.test3 = function() {
setTimeout(function() {
this.prop1 = 'changed_test_2';
this.prop2 = 'changed2_test_2';
this.prop3 = 'changed3_test_2';
this.apply(); //trigger $digest loop
}.bind(this));
}
So, all of that together will look like that (you can run it or open fiddle):
// TEST app code
var app = angular.module('app', ['watch_utils']);
app.controller('TestWatch', ['$scope', 'TestService', 'WatchObj', TestWatchCtrl]);
function TestWatchCtrl($scope, testService, watch) {
$scope.prop1 = testService.prop1;
$scope.prop2 = testService.prop2;
$scope.prop3 = testService.prop3;
watch(testService, ['prop1', 'prop2'], $scope, $scope);
$scope.test1 = function() {
testService.test1();
};
$scope.test2 = function() {
testService.test2();
};
$scope.test3 = function() {
testService.test3();
};
}
app.service('TestService', ['apply', TestService]);
function TestService(apply) {
this.apply = apply;
this.reset();
}
TestService.prototype.reset = function() {
this.prop1 = 'unchenged';
this.prop2 = 'unchenged2';
this.prop3 = 'unchenged3';
}
TestService.prototype.test1 = function() {
this.prop1 = 'changed_test_1';
this.prop2 = 'changed2_test_1';
this.prop3 = 'changed3_test_1';
}
TestService.prototype.test2 = function() {
setTimeout(function() {
this.prop1 = 'changed_test_2';
this.prop2 = 'changed2_test_2';
this.prop3 = 'changed3_test_2';
}.bind(this));
}
TestService.prototype.test3 = function() {
setTimeout(function() {
this.prop1 = 'changed_test_2';
this.prop2 = 'changed2_test_2';
this.prop3 = 'changed3_test_2';
this.apply();
}.bind(this));
}
//END TEST APP CODE
//WATCH UTILS
var mod = angular.module('watch_utils', []);
mod.service('apply', ['$timeout', ApplyService]);
function ApplyService($timeout) {
return function apply() {
$timeout(function() {});
};
}
mod.service('WatchObj', ['$rootScope', WatchObjService]);
function WatchObjService($rootScope) {
// target not always equals $scope, for example when using bindToController syntax in
//directives
return function watch_obj(obj, fields, target, $scope) {
// if $scope is not provided, $rootScope is used
$scope = $scope || $rootScope;
var watched = fields.map(function(field) {
return $scope.$watch(
function() {
return obj[field];
},
function(new_val) {
target[field] = new_val;
}
);
});
var unregister = function unregister_watch_obj() {
watched.map(function(unregister) {
unregister();
});
};
$scope.$on('$destroy', unregister);
return unregister;
};
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div class='test' ng-app="app" ng-controller="TestWatch">
prop1: {{prop1}}
<br>prop2: {{prop2}}
<br>prop3 (unwatched): {{prop3}}
<br>
<button ng-click="test1()">
Simple props change
</button>
<button ng-click="test2()">
Async props change
</button>
<button ng-click="test3()">
Async props change with apply
</button>
</div>

Resources