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.
Related
Pardon if this question is a total blow-off... Just getting warmed-up into the world angularJS.
I have these two controllers: seekerController and wizardController...
Inside the wizardController, I have a chat Scope object, and I have implemented a bunch of functions that are manipulating this chat Scope object.
Going back to the other controller now, ( seekerController ), I discover that I need to have basically a direct replica of this chat Scope object and all the other functions manipulating it as I have inside wizardController
The obvious way is just to copy all these into my other controller, and my work is done under a minute, but then I'll have a lot of repeated stuffs everywhere...
So: I'm looking for a way where I can have this(the code) in a single place, but still be able to have access to this chat Scope object from both controllers, as well as all the other functions working seamlessly.
Update - add code samples:
//seekerController
angular.module('cg.seeker', [])
.controller('SeekerController', ['$scope', 'seekerService', 'timeService', 'chatService', '$stateParams', 'toastr',
function ($scope, seekerService, timeService, chatService, $stateParams, toastr) {
...
// THE CHAT BUSINESS
$scope.chat = { close: true };
chatService.unreadCount(function(count){
$scope.chat.unreadCount = count;
$scope.$apply();
});
chatService.listDialogs( function (dialogList) {
$scope.chat.dialogList = dialogList.items;
$scope.$apply();
} );
$scope.endChat = function () {
$scope.chat.close = true;
}
$scope.chatBox = function (dialogId, occupants_ids) {
$scope.chat.opponentId = getOpponentId(occupants_ids);
chatService.getMessages( dialogId, function (messageList) {
$scope.chat.messages = messageList.items;
$scope.chat.close = false;
$scope.$apply();
});
}
var getOpponentId = function (opponentId) {
if(typeof(opponentId) != 'object') {
return opponentId;
} else {
return opponentId.filter(function(x) { return x != $scope.seeker.chat_user.chat_id_string; })[0];
}
}
$scope.sendMsg = function (opponentId) {
var msg = {
type: 'chat',
body: $scope.chat.msg,
extension: {
save_to_history: 1,
}
};
chatService.sendMsg(opponentId, msg);
$scope.chat.msg = '';
}
...
I now have an exact replica of the above code in a second controller WizardController. Exactly same, with no changes... and even a third controller have some of these, though not all.
The next level of abstraction to angularjs controllers are
Factory
Service
Provider
You could use a service called maybe chatService which could contain the common code. You can inject the service into any controller which needs the common functionality and invoke the methods on the Service.
Do note that you could use any of the above three options even though I have mentioned just Service in the above statement.
EDIT 1:
You could move the common parts of the code from Controller to Service.
For example:- You could move the construction of msg object from controller to chatService. You controller would be simply -
$scope.sendMsg = function (opponentId) {
chatService.sendMsg(opponentId);
$scope.chat.msg = '';
}
And your chatService would be doing the hard-work.
$chatService.sendMsg = function (opponentId) {
var msg = {
type: 'chat',
body: $scope.chat.msg,
extension: {
save_to_history: 1,
}
};
sendMsg(opponentId, msg);
}
After simplifying the Controllers you could revisit to see if you could use only one controller instead of 3 as they seem to be doing similar function.
I have a firebaseObject (MyFirebaseService.getCurrentUser()) bind to $scope.user.
After binding successful, I loop tho the object to see if the object contain "associatedCourseId" equal to some value ($stateParams.id). If does, the $scope.finishLessonCount count up. The problem is, when I add new Object inside the firebaseObject (that bindto user) via other page OR inside firebase, the finishLessonCount value won't change as what I expect for 3 way binding. I need to refresh the page to see the finishLessonCount reflect the true value. What is wrong? I want the finishLessonCount change using the compare function as I add more finishedLessons into the firebaseObject. Please see code below:
MyFirebaseService.getCurrentUser().$bindTo($scope, "user").then(function(){
for (var key in $scope.user.finishedLessons) {
if ($scope.user.finishedLessons.hasOwnProperty(key)) {
if ($scope.user.finishedLessons[key].associatedCourseId == $stateParams.id) {
$scope.finishLessonCount++;
}
}
};
console.log ($scope.finishLessonCount);
});
UPDATE 1 according to #Kato solution:
I decide to use Extending firebaseOject way to solute this problem. But still, it does not. I did not use factory here to simplify thing since I need to pass in courseId to do the operation. Here is my code:
function countLessons(lessons, courseId) {
var count = 0;
for(var key in lessons) {
if( lessons[key].associatedCourseId == courseId) {
count++;
}
}
return count;
}
var UserWithLessonsCounter = $firebaseObject.$extend({
$$updated: function(snap) {
var changed = $firebaseObject.prototype.$$updated.call(this, snap);
this.lessonCount = countLessons(this.finishedLessons, $stateParams.id);
}
});
var refTemp = new Firebase($rootScope.baseUrl + "users/" + $rootScope.userId);
var userTemp = new UserWithLessonsCounter(refTemp);
userTemp.$bindTo($scope, "userTemp").then(function(){
console.log($scope.userTemp);
});
userTemp.$watch(function() {
console.log("Does this run at all? " + $scope.userTemp.lessonCount);
});
I update the user object, the lessonCount value did not change unless I refresh the page. And the console.log inside $watch did not run at all. What is wrong?
The promise returned by $bindTo is called exactly once. It's not an event listener. You can't listen to this to get updated each time there is a change.
Please read the guide, start to finish, and read about Angular's $watch method before continuing down this route, as with some fundamental knowledge, this should not have been your first instinct.
A beginner approach would be to utilize $watch:
MyFirebaseService.getCurrentUser().$bindTo($scope, "user");
$scope.$watch('user', function() {
for (var key in $scope.user.finishedLessons) {
if ($scope.user.finishedLessons.hasOwnProperty(key)) {
if ($scope.user.finishedLessons[key].associatedCourseId == $stateParams.id) {
$scope.finishLessonCount++;
}
}
};
console.log ($scope.finishLessonCount);
});
Or, having familiarized with the AngularFire API, one might pick $scope.user.$watch() in place of the scope method, which would prove more efficient.
Having written a large portion of the AngularFire code, I would pick the $extend tool, which was added precisely for use cases like this:
// making some assumptions here since you haven't included
// the code for your firebase service, which does not seem SOLID
app.factory('UserWithLessonsCounter', function($firebaseObject) {
return $firebaseObject.$extend({
$$updated: function(snap) {
var changed = $firebaseObject.prototype.$$updated.call(this, snap);
this.lessonCount = countLessons(this.finishedLessons);
return changed;
}
});
});
function countLessons(lessons) {
var count = 0;
for(var key in lessons) {
if( lessons.hasOwnProperty(key) ) {
count++;
}
}
return count;
}
And now in your controller:
app.controller('...', function($scope, UserWithLessonsCounter) {
var ref = new Firebase(...);
var user = new UserWithLessonCounter(ref);
user.$bindTo($scope, 'user');
user.$watch(function() {
console.log($scope.user.lessonCount);
});
});
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.
I have a situation in which I want to monitor a specific variable.
$scope.$watch('action', function() { /* do something */ }
However, I only want to do something if $scope.id didn't change (if $scope.action changes it is possible that $scope.id has changed too.)
The only solution I can think of is
$scope.$watch('action', function() {
if(idHasChanhed === false){
/* do something */
}
idHasChanged = false;
});
$scope.$watch('id', function() { idHasChanged = true }
However, I was wondering if angular has a better solution than this, and I don't know if this solution will always work ( is the order in which the $watches are executed random !?)
My solution :
$scope.$watch('[action,id]', function() {
// called when action or id changed
});
Info : The string concatenation action + id will fail.
var action,id; // variables you need to watch
$scope.$watch('action + id', function() {
// actions
});
$watchGroup This function is introduced in Angular1.3. This works the same as $watch() function except that the first parameter is an array of expressions to watch.
Use $watchGroup
$scope.$watchGroup(['action', 'id'], function(newVal, oldVal) {
//write your code here
});
I am having a really hard time deciphering what is going on here. I understand the basics of Angular's $digest cycle, and according to this SO post, I am doing things correctly by simply assigning a scoped var to a service's property (an array in this case). As you can see the only way I can get CtrlA's 'things' to update is by re-assigning it after I've updated my service's property with a reference to a new array.
Here is a fiddle which illustrates my issue:
http://jsfiddle.net/tehsuck/Mujun/
(function () {
angular.module('testApp', [])
.factory('TestService', function ($http) {
var service = {
things: [],
setThings: function (newThings) {
service.things = newThings;
}
};
return service;
})
.controller('CtrlA', function ($scope, $timeout, TestService) {
$scope.things = TestService.things;
$scope.$watch('things.length', function (n, o) {
if (n !== o) {
alert('Things have changed in CtrlA');
}
});
$timeout(function () {
TestService.setThings(['a', 'b', 'c']);
// Without the next line, CtrlA acts like CtrlB in that
// it's $scope.things doesn't receive an update
$scope.things = TestService.things;
}, 2000);
})
.controller('CtrlB', function ($scope, TestService) {
$scope.things = TestService.things;
$scope.$watch('things.length', function (n, o) {
if (n !== o) {
// never alerts
alert('Things have changed in CtrlB');
}
});
})
})();
There are two issues with your code:
Arrays don't have a count property; you should use length instead.
$scope.$watch('things.length', ...);
But there's a caveat: if you add and remove elements to/from the things array and end up with a different list with the same length then the watcher callback won't get triggered.
The setThings method of TestService replaces the reference to the things array with a new one, making TestService.things point to a new array in memory while both CtrlA.$scope.things and CtrlB.$scope.things remain pointing to the old array, which is empty. The following code illustrates that:
var a = [];
var b = a;
a = [1, 2, 3];
console.log(a); // prints [1, 2, 3];
console.log(b); // prints [];
So in order for you code to work you need to change the way TestService.setThings updates its things array. Here's a suggestion:
setThings: function (newThings) {
service.things.length = 0; // empties the array
newThings.forEach(function(thing) {
service.things.push(thing);
});
}
And here's a working version of your jsFiddle.
I don't really know why, but it seems to be corrected if you use a function to return the data in your service, and then you watch that function instead of the property. As it seems unclear, you can see it here : http://jsfiddle.net/DotDotDot/Mujun/10/
I added a getter in your service :
var service = {
things: [],
setThings: function (newThings) {
service.things = newThings;
},
getThings:function(){
return service.things;
}
};
then, I modified your code in both controller by this :
$scope.things = TestService.getThings();
$scope.getThings=function(){return TestService.getThings();};
$scope.$watch('getThings()', function (n, o) {
if (n !== o) {
// never alerts
alert('Things have changed in CtrlA');
}
}, true);
and in the HTML :
<li ng-repeat="thing in getThings()">{{thing}}</li>
It defines a function getThings, which will simply get the property in your service, then I watch this function (AFAIK $watch do an eval on the parameter, so you can watch functions), with a deep inspection ( the true parameter at the end). Same thing in your other controller. Then, when you modifies the value of your service, it is seen by the two $watchers, and the data is binded correctly
Actually, I don't know if it's the best method, but it seems to work with your example, so I think you can look in this way
Have fun :)