view raw
Robert Holden Robert Holden - 9 months ago 70
AngularJS Question

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: "="
}
});

Answer

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.