$watching data in a service in AngularJS - javascript

I have here a service sharedData and a controller HostController. I'm looking to have the HostController watch the saveDataObj variable in the sharedData service, and then assign that value to host.dataOut, which is then reflected in the view as an ngModel attribute.
My issue is that I don't really know how to use $watch. I want host.dataOut to be updated every time saveDataObj in the sharedData service is changed (or at least, each time setData is called).
The following code produces a line in the console:
sharedData.getData(): Object { }
This is to be expected during initialisation. However, when I call setData via another controller to change saveDataObj to a different value, nothing is logged to the console.
What am I doing wrong?
angular.module('app', [])
.factory('sharedData',function(){
var saveDataObj = {};
return {
getData: function(){
return saveDataObj;
},
setData: function(data){
saveDataObj = data;
}
};
})
.controller('HostController',['$scope','sharedData',function($scope,sharedData){
$scope.sharedData = sharedData;
var host = this;
host.dataOut = {};
$scope.$watch(
'sharedData.getData()',
function handleDataChange(newValue,oldValue) {
console.log("sharedData.getData():",newValue);
host.dataOut = newValue;
}
);
}])

.controller('HostController',['$scope','sharedData',function($scope,sharedData){
$scope.sharedData = sharedData;
var host = this;
host.dataOut = {};
$scope.serviceData = sharedData.getData(); //local variable
$scope.$watch('serviceData', function(newValue, oldValue) {
console.log("sharedData.getData():",newValue);
host.dataOut = newValue;
}
);
}]);
Hope help you!

Answer from me, the OP:
The issue was not that I was trying to $watch something in a service - it was that I was trying to $watch an Object. For this, I needed to declare the third term of $watch, objectEquality, to be true.
$scope.$watch(
'sharedData.getData()',
function handleDataChange(newValue,oldValue) {
console.log( "sharedData.getData():", newValue );
},
true
);
https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watch
I'm going to mark this as best answer because I'm a narcissist.

Related

Controller change data of another controller on click

I want to pass some data from one controller to a second one via an onClick-Event. I tried to use a service between the two controllers but it seems that the controller who receives the data from the service doesn't recognize the onClick-Event of the first controller which leads to static/non changing data.
OnClick function (Controller 1)
$scope.select = function(index){
vm.currentActive = index;
sessionService.setState(index);
};
Exchange service
app.service('sessionService', function() {
var state = null;
var setState = function(changestate){
state = changestate;
};
var getState = function(){
return state;
};
return {
setState: function(changestate){
setState(changestate);
},
getState: function(){
return state;
}
};
});
Receiving Controller (Controller 2)
app.controller('ContentController', function ($scope, sessionService)
{
var vm = this;
vm.currentActive = sessionService.getState();
});
In the end I want that the state of Controller 2 changes whenever the OnClick-Event is triggered in controller 1. Is this way with the service the best or what do recommend to change the data in controller 2 after a click ?
One option for watching the state of a service is to use $scope.$watch with a function that returns the value to be watched for changes.
$scope.$watch(function(){ return sessionService.getState(); }, function(newValue, oldValue){
//Do something
});
If the value in the service is changed, the watch will pick up the change on the next digest cycle. With this method there's no need to have your service or other controller try and signal that the value has changed.
If your service's getter method does not depend on this, you can simplify the watcher by just passing the getter method as the watch function rather than using a wrapper function.
$scope.$watch(sessionService.getState, function(newValue, oldValue){
//Do something
});
You can add onChange event to service:
app.service('sessionService', function() {
var state = null;
var callbacks = [];
var setState = function(changestate) {
callbacks.forEach(function(callback) {
callback(state, changestate);
});
state = changestate;
};
var getState = function() {
return state;
};
return {
setState: function(changestate) {
setState(changestate);
},
getState: function() {
return state;
},
onChange: function(fn) {
if (typeof fn == 'function') {
callbacks.push(fn);
}
}
};
});
The reason your Receiving Controller is not getting the updated value is because the state property is copied into vm.state at the point of the directive definition object's initialization.
vm.currentActive = sessionService.getState();
Here, getState is only called once, so it won't matter if that state value is later updated...
One Solution
One option would be to call getState from the controller's view (which will get re-called (i.e. the value will be updated) with every digest cycle)...note this strategy has performance implications...
Another Solution
Another option is to leverage the trickle down effect of referenced objects (or as Miško explains in this Angular Best Practices video, "...if you don't have a dot, you're doing it wrong..."
You could utilize this strategy by using an object to store the state in your Exchange Service...
app.service('sessionService', function() {
var data = {};
var setState = function(changestate){
data.state = changestate;
};
var getState = function(){
return data.state;
};
return {
setState: setState,
data: data
};
});
Receiving Controller
app.controller('ContentController', function ($scope, sessionService) {
var vm = this;
vm.data = sessionService.data;
});
Then whenever data.state is updated in sessionService, vm.data.state will (by virtue of referenced data) contain the updated data as well.
In other words, vm.data.state will always contain the most up to date value of sessionService.data.state because they both refer to the same object.

Why isn't $scope updated when bound properties of services are updated by other services?

I have one service for handling data, and one for logic.
app.service('DataService', function() {
this.stuff = [false];
this.setStuff = function(s){
this.stuff = angular.copy(s);
}
});
The data service has a set function and a data property.
app.service('LogicService', function(DataService, $http) {
DataService.setStuff(["apple", "banana"]);
$http.get("./data.json").then(function(res){
DataService.setStuff(res.data.stuff);
});
});
I am assigning a property of the data service to the controller for binding to the DOM.
app.controller('MainCtrl', function($scope, DataService, LogicService ) {
$scope.message = "Hello, World!";
$scope.stuff = DataService.stuff;
//This is the only way I could get it to work, but isn't this JANKY?
//$scope.$watch(
// function(){
// return DataService.stuff
// },
// function(n,o){
// $scope.stuff = n;
// })
})
If I 'seed' the data service when the logic service is instantiated, and then later update it following an $http call, the DOM reflects the 'seeded' or initial value, but does not update.
Is there something fundamental I am missing in my understanding of the digest loop?
If I add a $watch function in my controller, all is well, but this seems yucky.
//FIXED//
#scott-schwalbe 's method of using Object.asign() works nicely, preserves my original structure, and is one line.
this.setStuff = function(s){
Object.assign(this.stuff, s);
}
Working Plunker
(sorry for titlegore)
If your data property is an object and is binded to the scope, then the scope will update whenever the object changes as long as you don't dereference it (eg data = x). Are you reassigning data object on the $http call?
An alternative to your current code to keep the reference using Object.assign
app.service('DataService', function() {
this.stuff = [false];
this.setStuff = function(s){
Object.assign(this.stuff, s);
}
});
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope, DataService) {
$scope.message = "Hello, World!";
//Get stuff data from your service, this way you stuff lives in your service
//And can be accessed everywhere in your app.
//It also makes your controller thin. Which is the top priority
$scope.stuff = DataService.getStuff();
//Or async
DataService.getStuffAsync()
.then(function(val){
$scope.asycStuff = val;
});
this.clickFromAButton = function(){
DataService.setStuff(["apple", "banana"]);
};
});
app.service('DataService', function() {
this.stuff = [false];
this.asyncStuff;
this.setStuff = function(s){
this.stuff = angular.copy(s);
};
this.getStuff = function(){
return this.stuff;
};
this.getStuffAsync = function(){
//If i already fetched the data from $http, get it from the service.
return this.asyncStuff || $http.get("./data.json").then(function(res){
//When i fetch it for the first time I set the data in my service
this.asyncStuff = res.data;
//and I return the data
return res.data;
});
};
});
This is a good 'pattern' to follow ;)
Instead of putting "stuff" on scope. Put your DataService object on scope.
app.controller('MainCtrl', function($scope, DataService, LogicService ) {
$scope.message = "Hello, World!";
$scope.DataService = DataService;
//$scope.stuff = DataService.stuff;
HTML
<body ng-controller="MainCtrl">
{{DataService.stuff}}
</body>
The $interpolate service will automatically places a $watch on DataService.stuff. Thus there is no need to do it inside your controller.
The DEMO on PLNKR.

How to react to change in data in a service in angularjs on a controller

I want to be able to share data between two controllers so that I can send a boolean to the service from the first controller which is turn triggers a change in the second controller.
Here is what the service looks like
exports.service = function(){
// sets Accordion variable to false ;
var property = true;
return {
getProperty: function () {
return property;
},
setProperty: function(value) {
property = value;
}
};
};
Now the first controller
exports.controller = function($scope, CarDetailsService, AccordionService ) {
$scope.saveDetails = function() {
AccordionService.setProperty(false);
}
}
and the second one
exports.controller = function($scope, AccordionService ) {
$scope.isCollapsed = AccordionService.getProperty();
}
The use case is that when i click on a button on the first controller,the service updates the data inside it, which is then served on the second controller, thus triggering a change in the second controller.
I have been looking around for quite some time but couldn't find a solution to this. Maybe im just stupid.
On the second controller you can $watch the variable you change in the first:
scope.$watch('variable', function(newValue, oldValue) {
//React to the change
});
Alternatively, you can use the $broadcast on the rootScope:
On the first controller:
$rootScope.$broadcast("NEW_EVENT", data);
On the other controller:
scope.$on("NEW_EVENT", function(event, data){
//use the data
});

Angular binding to service value not updating

I cannot get a binded service value to update when it is changed. I have tried numerous methods of doing so but none of them have worked, what am I doing wrong? From everything I have seen, this seems like it should work...
HTML:
<div class="drawer" ng-controller="DrawerController">
{{activeCountry}}
</div>
Controller:
angular.module('worldboxApp')
.controller('DrawerController', ['$scope', 'mapService', function($scope, mapService) {
$scope.$watch(function() { return mapService.activeCountry }, function(newValue, oldValue) {
$scope.activeCountry = mapService.activeCountry;
});
}]);
Service:
angular.module('worldboxApp').
service('mapService', function(dbService, mapboxService, userService) {
this.init = function() {
this.activeCountry = {};
}
this.countryClick = function(e) {
this.activeCountry = e.layer.feature;
};
this.init();
});
I put a break point to make sure the mapService.activeCountry variable is being changed, but all that ever shows in the html is {}.
If you work with objects and their properties on your scope, rather than directly with strings/numbers/booleans, you're more likely to maintain references to the correct scope.
I believe the guideline is that you generally want to have a '.' (dot) in your bindings (esp for ngModel) - that is, {{data.something}} is generally better than just {{something}}. If you update a property on an object, the reference to the parent object is maintained and the updated property can be seen by Angular.
This generally doesn't matter for props you're setting and modifying only in the controller, but for values returned from a service (and that may be shared by multiple consumers of the service), I find it helps to work with an object.
See (these focus on relevance to ngModel binding):
https://github.com/angular/angular.js/wiki/Understanding-Scopes
If you are not using a .(dot) in your AngularJS models you are doing it wrong?
angular.module('worldboxApp', []);
/* Controller */
angular.module('worldboxApp')
.controller('DrawerController', ['$scope', 'mapService',
function($scope, mapService) {
//map to an object (by ref) rather than just a string (by val), otherwise it's easy to lose reference
$scope.data = mapService.data;
$scope.setCountry = setCountry; //see below
function setCountry(country) {
// could have just set $scope.setCountry = mapService.setCountry;
// however we can wrap it here if we want to do something less generic
// like getting data out of an event object, before passing it on to
// the service.
mapService.setCountry(country);
}
}
]);
/* Service */
angular.module('worldboxApp')
.service('mapService', ['$log',
function($log) {
var self = this; //so that the functions can reference .data; 'this' within the functions would not reach the correct scope
self.data = {
activeCountry: null
}; //we use an object since it can be returned by reference, and changing activeCountry's value will not break the link between it here and the controller using it
_init();
function _init() {
self.data.activeCountry = '';
$log.log('Init was called!');
}
this.setCountry = function _setCountry(country) {
$log.log('setCountry was called: ' + country);
self.data.activeCountry = country;
}
}
]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.28/angular.min.js"></script>
<div ng-app="worldboxApp">
<div ng-controller="DrawerController">
<button ng-click="setCountry('USA')">USA</button>
<br />
<button ng-click="setCountry('AUS')">AUS</button>
<br />Active Country: {{data.activeCountry}}
</div>
</div>
In some case $watch is not working with factory object. Than you may use events for updates.
app.factory('userService',['$rootScope',function($rootScope){
var user = {};
return {
getFirstname : function () {
return user.firstname;
},
setFirstname : function (firstname) {
user.firstname = firstname;
$rootScope.$broadcast("updates");
}
}
}]);
app.controller('MainCtrl',['userService','$scope','$rootScope', function(userService,$scope,$rootScope) {
userService.setFirstname("bharat");
$scope.name = userService.getFirstname();
$rootScope.$on("updates",function(){
$scope.name = userService.getFirstname();
});
}]);
app.controller('one',['userService','$scope', function(userService,$scope) {
$scope.updateName=function(){
userService.setFirstname($scope.firstname);
}
}]);
Here is the plunker
Note:- In Some case if broadcast event is not fired instantly you may use $timeout. I have added this in plunker and time depends on your needs. this will work for both factories and services.

AngularJS: Reassigning an object vs. replacing it in a controller

I am building a subscriber/observer pattern for displaying data in realtime for my angular app.
The observer is built with a factory injected into the angular controller and whose role is to fetch data and update it. The basic code structure can he found in this fiddle: http://jsfiddle.net/ctrager/67QR7/3/
var myApp = angular.module('myApp', [])
.factory('MyFactory', [function () {
var Collection = {};
Collection.isLoaded = 0;
Collection.data = [1, 2];
Collection.username = "corey and eric";
Collection.update = function () {
Collection.data.push(new Date())
}
Collection.replace = function () {
// If you do Collection.data = []
// here you are doing the same thing
// as the empty collection bug. I can't
// tell you EXACTLY why this confuses angular
// but I'm 99% sure it's the same phenomenon
Collection.data = [new Date()]
}
Collection.replace_fixed = function () {
// This works
Collection.data.length = 0
Collection.data.push(new Date())
}
return Collection;
}])
function MyCtrl($scope, MyFactory) {
$scope.name = 'Eric';
$scope.items = MyFactory.data;
$scope.replace = function(){
console.log("replace")
MyFactory.replace()
//$scope.items = MyFactor.data;
}
$scope.replace_fixed = function(){
console.log("replace_fixed")
MyFactory.replace_fixed()
//$scope.items = MyFactor.data;
}
$scope.update = function(){
console.log("update")
MyFactory.update()
}
}
The factory (MyFactory) contains a collection (Collection.data). Any push (/splice) to that collection is reflected in the scope, but if I replace the entire collection (Collection.replace()) the change is no longer reflected in $scope. Any idea why?
This works:
http://jsfiddle.net/67QR7/4/
changed the thing stored on scope to be the factory instead of data. then the html repeat to do items.data.
So it looks like this is because you replaced the reference inside collection, but that doesn't change where $scope.items was pointing to.
So you are creating a reference to MyFactory.data from $scope.items. Angular puts a $watch on $scope.items and looks for changes. When you call MyFactory.replace, you change MyFactory.data, but $scope.items remains the same. So as far as your watch is concerned, nothing has happened.
You can fix this by using replace_fixed, or watch for changes to MyFactory.data. http://jsfiddle.net/KtB93/
$scope.MyFactory = MyFactory;
$scope.$watch("MyFactory.data", function(newData) {
console.log('myFactory.data changed');
$scope.items = newData;
});
Or alternatively (probably better), you can use a function as the watch expression so you don't have to plop MyFactory on the scope (http://jsfiddle.net/XAW54/1/):
$scope.$watch(function() {
return MyFactory.data;
}, function(newData) {
$scope.items = newData;
});

Categories