AngularJS $watch not behaving as expected - javascript

I have a directive that should call a function whenever a service variable is changed.
The following code is inside the directive:
$rootScope.$watch('movesService.selectedMove', function() {
console.log(movesService.selectedMove);
if (movesService.selectedMove === "Punch") {
vm.pushToFightLog('Select Target');
}
if (movesService.selectedMove === "Fury") {
//Action
}
if (movesService.selectedMove === "Fortify") {
//Action
}
if (movesService.selectedMove === "Parry") {
//Action
}
}, true);
Service:
angular
.module('outerZone')
.service('movesService', movesService);
function movesService() {
var vm = this;
vm.selectedMove = "Punch";
}
The thing is that when the $watch is called the first time it is able to read the variable and log it to the console, but even when the variable is changed after that, it does not fire the function.
I'm fairly confident that $rootScope is properly injected, but here is the code just to double check.
angular
.module('outerZone')
.directive('fightDisplay', fightDisplay);
fightDisplay.$inject = ["alliesService", "enemiesService", "fightQueueService", "$timeout", "movesService", "$rootScope"];
function fightDisplay(alliesService, enemiesService, fightQueueService, $timeout, movesService, $rootScope) {

With the plunker you have provided,I've made some changes in the watch function as below.
$scope.$watch('test', function() { //scope variable should be watched like this
$scope.watchCount++
});//removed true for strict checking (===) as $scope.test is initalised as String 'Hello'
if we want a Strict checking then change $scope.test =0 and
$scope.$watch('test', function() {
$scope.watchCount++
},true);
Here is the working plunker.To know more how $watch() works check this blog

Related

Angularjs watch not working with array inside of object

I have an object {Client:[],Employee:[],Product:[],Project:[],PayPeriod:[]} in which each array gets pushed and spliced by components through a two way binding. The main controller connects all 5 of the arrays and gives them to another component. In said component I need to watch that binding but no matter what I do it does not work. This is what I have now.
$scope.$watch('ctrl.parameters', ctrl.Update(), true);
ctrl.Update(); is a function and works.
ctrl.parameters does get updated but does not trigger $watch.
It's a bit of a complicated so if you need anything explained butter I can.
ctrl.Update = function () {
$.post("/TrackIt/Query.php?Type=getViaParams&EntityType="+ctrl.entity,{Params:ctrl.parameters},function(Data,Status){
if(Status=="success"){
if (Data.Success) {
ctrl.List = Data.Result.Entities;
} else {
AlertService.Alert(Data.Errors[0],false,null);
SessionService.Session(function () {
ctrl.Update();
});
}
$scope.$apply();
}else{
AlertService.Alert("Something is up with the select options",false,null);
}
},'json');
};
Edit 1 :
Par = {Client:[],Employee:[],Product:[],Project:[],PayPeriod:[]}
5 Components with two way binding = Par.X (these are what edit the parameters)
1 Component with two way binding = Par (I need to watch the binding inside here)
Edit 2 :
<script>
TrackIT.controller('EntryController', function EntryController($scope, $http, AlertService, SessionService, DisplayService) {
$scope.Parameters = {Client:[],Employee:[],Product:[],Project:[],PayPeriod:[]};
$scope.Values = {};
});
</script>
<style>
entity-select{
float: left;
display: inline;
padding: 0 5px;
}
#SelectParameters{
float: left;
}
</style>
<div ng-app="TrackIT" ng-controller="EntryController">
<div id="SelectParameters">
<entity-select entity="'Client'" ng-model="Values.Client" multi="true" ng-array="Parameters.Client"></entity-select>
<entity-select entity="'Employee'" ng-model="Values.Employee" multi="true" ng-array="Parameters.Employee"></entity-select>
<entity-select entity="'Product'" ng-model="Values.Product" multi="true" ng-array="Parameters.Product"></entity-select>
<entity-select entity="'Project'" ng-model="Values.Project" multi="true" ng-array="Parameters.Project"></entity-select>
<entity-select entity="'PayPeriod'" ng-model="Values.PayPeriod" multi="true" ng-array="Parameters.PayPeriod"></entity-select>
</div>
<br>
<parameter-table entity="'Entry'" parameters="Parameters"></parameter-table>
</div>
TrackIT.component('entitySelect', {
templateUrl: "/Content/Templates/Select.html",
controller: function SelectController($scope, $http, AlertService, SessionService) {
var ctrl = this;
ctrl.Options = [];
ctrl.Display = [];
ctrl.Add = function () {
var Display = {'Label':ctrl.Label(ctrl.ngModel),'Value':ctrl.ngModel};
ctrl.ngArray.push(ctrl.ngModel);
ctrl.Display.push(Display);
};
ctrl.Remove = function (Key) {
ctrl.ngArray.splice(Key, 1);
ctrl.Display.splice(Key, 1);
};
ctrl.$onInit = function() {
$.post("/TrackIt/Query.php?Type=getSelectList&EntityType="+ctrl.entity,null,function(Data,Status){
if(Status=="success"){
if (Data.Success) {
ctrl.Options = Data.Result.Entities;
if(ctrl.ngModel==undefined){
if(ctrl.none){
ctrl.ngModel = "NULL"
}else{
ctrl.ngModel = angular.copy(ctrl.Options[0].Attributes.ID.Value.toString());
}
}
} else {
AlertService.Alert(Data.Errors[0],false,null);
}
$scope.$apply();
}else{
AlertService.Alert("Something is up with the select options",false,null);
}
},'json');
};
ctrl.Label = function(Value) {
for (var prop in ctrl.Options) {
if(!ctrl.Options.hasOwnProperty(prop)) continue;
if(ctrl.Options[prop].Attributes.ID.Value.toString()==Value.toString()){
return ctrl.Options[prop].DisplayName;
}
}
};
},
bindings: {
entity:"<",
multi:"<",
none:"<",
ngModel:"=",
ngArray:"="
}
});
TrackIT.component('parameterTable', {
templateUrl: "/Content/Templates/BasicTable.html",
controller: function ParameterTableController($scope, $http, AlertService, SessionService, DisplayService) {
var ctrl = this;
ctrl.List = {};
ctrl.Update = function () {
$.post("/TrackIt/Query.php?Type=getViaParams&EntityType="+ctrl.entity,{Params:ctrl.parameters},function(Data,Status){
if(Status=="success"){
if (Data.Success) {
ctrl.List = Data.Result.Entities;
} else {
AlertService.Alert(Data.Errors[0],false,null);
SessionService.Session(function () {
ctrl.Update();
});
}
$scope.$apply();
}else{
AlertService.Alert("Something is up with the select options",false,null);
}
},'json');
};
$scope.$watch('ctrl.parameters', ctrl.Update.bind(ctrl), true);
ctrl.$onInit = function() {
DisplayService.DisplayTrigger(function () {
ctrl.Update();
});
ctrl.Update();
}
},
bindings: {
entity: "<",
parameters: "="
}
});
There are two problems here.
Problem 1: ctrl is not a property on the scope
After seeing the full controller code, I can see that ctrl is just an alias for this, the instance of the controller which will be published on the scope as $ctrl by default. But you can avoid having to worry about what it is called by instead passing a function instead of a string to $scope.$watch():
// ES5
$scope.$watch(function () { return ctrl.parameters; }, ctrl.Update, true);
// ES6/Typescript/Babel
$scope.$watch(() => ctrl.parameters, ctrl.Update, true);
It's all functions to Angular
You may not be aware that as far as Angular is concerned, it is always calling a function for each watch to get the value to compare. When you pass a string to $scope.$watch(), Angular uses $parse to create a function from that expression. This is how Angular turns strings into executable code in bindings, expressions, and so on.
The function that gets created takes in a single parameter, which is the "context" to evaluate the expression on. You can think of this as which scope to use.
When you pass a function to $scope.$watch() as the first parameter, you effectively save Angular having to create a function for you from the string.
Problem 2: the way you specify the watch listener function
Your ctrl.Update() function is just a function that you want run whenever ctrl.parameters changes.
What you have said in your code of $scope.$watch('ctrl.parameters', ctrl.Update(), true); is:
Do a deep watch (watch changes to any property) on ctrl.parameters, and when it changes, call the result of calling ctrl.Update(), which will be a jQuery promise, not a function.
Instead, you want to pass the ctrl.Update function itself as the second parameter to $scope.$watch(), so it gets called when a change is detected. To do that, just pass ctrl.Update instead of ctrl.Update():
$scope.$watch('ctrl.parameters', ctrl.Update, true);
A Note of Caution
Using ctrl.Update in this particular case will work, because there is no use of this inside that function. For others looking at this answer, note that when you pass a function in this way, the this binding (the "context") is not maintained as ctrl as you might expect. To get around this, use ctrl.Update.bind(ctrl), or just wrap it in a function so it gets called with the correct context: $scope.$watch('ctrl.parameters', function () { ctrl.Update() }, true);.
Use deep/value watches sparingly
You should be very sparing in your use of deep watches in an Angular app (also known as value watches). The reason is that it is a very expensive operation for big objects, as Angular has to do a deep comparison of the object on every digest cycle - traversing through every single property on the entire object, and then, if there is a change, making a deep clone of the object, which again requires traversing every single property to make a completely separate copy to compare against next time.
You can think of a deep watch on an object with n properties as being the equivalent of n shallow/reference watches.
I have a feeling that may be a scarily large number in your situation.
I think the problem is that your watch statement is incorrect. The second parameter to $watch must be a function. The following should work:
$scope.$watch('ctrl.parameters', ctrl.Update.bind(ctrl), true);
Note the use of bind to ensure the this parameter is set appropriately.

Changing a model from a directive callback function in AngularJS has no effect

So, what I want is a custom directive which will read and clear the current selection, and then pass the selected text to a callback function. This works, but whatever I do in that callback function has no effect on the scope, which leads me to believe that there are multiple scopes, which are in conflict somehow.
First, I defined a directive like this:
angular.module('app').directive('onTextSelected', ['$window', function ($window) {
return {
restrict: 'A',
scope: {selectFn: '&'},
link: function (scope, element, attrs) {
$(element).mouseup(function () {
var selection = $window.getSelection().toString();
if ($window.getSelection().removeAllRanges) {
$window.getSelection().removeAllRanges();
} else if ($window.getSelection().empty) {
$window.getSelection().empty();
}
if (selection && selection.trim() !== "") {
scope.selectFn({
text: selection.trim()
});
}
});
}
};
}]);
It's used in the template as follows:
<pre ng-bind-html="message" id="messagePre" on-text-selected
select-fn="textSelected(text)"></pre>
And this is the callback function:
$scope.textSelected = function (text) {
console.log(text);
$scope.currentText = text;
};
I have a text box which uses $scope.textSelected as model, and setting it with the same code from another function works properly, but in this case it just doesn't. Nothing happens, although all the code gets executed (it prints on the console, for example).
It works after calling
$scope.$digest()
or using
$scope.$apply()
Probably related to the usage of jQuery here.

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.

Angular view / scope not updating even when apply is called

So, I have a global function thats called by a different library, that I'd like to impact my scope when its called.
In my controller I have:
angular.module('myApp')
.controller('myCtrl', function ($scope) {
$scope.nextAvailable = false;
$scope.thirdParty = window.thirdPartyFn = function() {
$scope.nextAvailable = true;
$scope.$apply();
};
});
When I log nextAvailable in the console after, it does return true (within that function obviously, so I'm confident its being called properly) - but it does not seem to update the surrounding scope within the enclosing controller. Any ideas?

Categories