Issues binding / watching service variable shared between two controllers - javascript

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 :)

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.

Deep copy (unbind) two scope variables - AngularJS

I'm trying to have two separate variables (data.newPictures, profile.pictures), one initialized with the values of the other.
Initialization runs well, but when I edit one runnig function removeNewPicture($index) I edit also the second one (like they're binded).
The expected result is the total independence of the two objects, so that function only removes data from data.newPictures, keeping profile.pictures unchanged.
Any ideas how to prevent this?
app.controller('ModelController', function($scope, $rootScope, $state, $stateParams, $models, $toast) {
$models.getModel($stateParams.uid)
.then((model) => {
$scope.profile = model;
$scope.data = {
edit: false,
newPictures: $scope.profile.pictures, // <<< Initialization
newBiography: $scope.profile.biography,
newFeatures: $scope.profile.features,
newStarting_fees: $scope.profile.starting_fees
};
})
.catch((err) => {
$toast.error(err.err);
if(err.code === 69) $state.go('login');
});
$scope.removeNewPicture = ($index) => {
$scope.data.newPictures.splice($index, 1); // <<< This edits both the objects
console.log($scope.data.newPictures, $scope.profile.pictures);
};
$scope.profile = {};
$scope.data = {};
});
Picture and newPicture reference the same array. Create a copy to newPictures to avoid this issue
newPictures: angular.copy($scope.profile.pictures),
They are basically the same object.
You should assign newPictures like $scope.profile.pictures.slice(0); in order to get the array copied.
Snippet would become:
$scope.data = {
edit: false,
newPictures: $scope.profile.pictures.slice(0),
newBiography: $scope.profile.biography,
newFeatures: $scope.profile.features,
newStarting_fees: $scope.profile.starting_fees
};
$scope.data.newPictures and $scope.profile.pictures refer the same object.
You should 'clone' the object you want to copy:
...
newPictures: jQuery.extend(true, {}, $scope.profile.pictures);
...

Can't access variable inside angular.js controller

This is my controller:
app.controller("PlaceController", ['$http', function($http){
this.places = shops;
var hotels = this;
hotels.objects = [];
this.spots = new Array;
this.newPlace = {};
this.city = new String();
this.addPlace = function() {
this.places.push(this.newPlace);
this.newPlace = {};
var request = *some query syntax, sorry for hiding*
$http.get(request).success(function(data) {
hotels.objects = data;
console.log(hotels.objects.elements);
});
for (each in hotels.objects.elements) {
this.spots.push(each.tags.name);
};
console.log(this.spots);
}}] );
I get an empty array when I log this.spots to the console. The http request etc work perfectly because the console.log(hotels.objects.elements) statement works perfectly.
Because of this problem, I can't output it into my HTML either. What should I do?
You are issuing an asynchronous request to get the spots, but you're logging them before they complete.
Change this.addPlace to log / act on the spots array inside the promise callback:
this.addPlace = function() {
this.places.push(this.newPlace);
this.newPlace = {};
var request = *some query syntax, sorry for hiding*
$http.get(request).success(function(data) {
hotels.objects = data;
console.log(hotels.objects.elements);
for (each in hotels.objects.elements) {
this.spots.push(each.tags.name);
};
console.log(this.spots);
});
You're adding to the spots array before the ajax request is done, move your calls to push inside the callback:
$http.get(request).success(function(data) {
hotels.objects = data;
console.log(hotels.objects.elements);
angular.forEach(hotels.objects.elements, function(value) {
hotels.spots.push(value.tags.name);
});
});
Also, you should really be using $scope instead of references to this. This would simplify your code a bit, without needing to rename this to hotels
full controller code using $scope
app.controller("PlaceController", ['$scope', '$http', function($scope, $http){
$scope.places = shops;
$scope.objects = [];
$scope.spots = new Array;
$scope.newPlace = {};
$scope.city = new String();
$scope.addPlace = function() {
$scope.places.push($scope.newPlace);
$scope.newPlace = {};
var request = *some query syntax, sorry for hiding*
$http.get(request).success(function(data) {
$scope.objects = data;
console.log($scope.objects.elements);
angular.forEach($scope.objects.elements, function(value, key) {
$scope.spots.push(value.tags.name);
});
// spots is ready, log it, do whatever
console.log($scope.spots);
});
}}] );
NOTE: Using $scope means you won't need to call this from your html to reference the objects and functions defined in your controller.
An example:
<div ng-controller="PlaceController">
<!-- no need to call places.city, if you use $scope just write city -->
{{city}}
</div>
EDIT: You probably shouldn't use JavaScript's for-in, the problem with it is that it iterates on the names or indexes of your objects/arrays.
An example:
var someArray = ['a', 'b', 'c'];
for (i in someArray) {
console.log(i); // prints 0, 1, 2
console.log(someArray[i]); // prints 'a', 'b', 'c'
}
This is different from any for-in/for-each implementation in other popular languages.
Anyway, in this case I've edited the code above to use Angular's forEach, which is a more appropriate solution (many libraries implement custom for-each functions to fix JS's weird for-in)
You can read more in Angular's docs
Another option, in plain javascript is, if $scope.objects.elements is an array, using the map() function, like this:
$scope.spots = $scope.objects.elements.map(function(value) {
return value.tags.name; // value is an item in object.elements
});
try this ..
due to your async call you need to perform task inside success
$http.get(request).success(function(data) {
hotels.objects = data;
console.log(hotels.objects.elements);
for (each in hotels.objects.elements) {
hotels.spots.push(each.tags.name);
};
console.log(this.spots);
});

Splitting a string gives different result when saved to variable and when console.log-ing it

I have a really simple function that is executed when the $location changes (see below).
The problem is, the occurrence of $location.path().split("/") in the assignment returns ["browser"], when $location.path() == "/browser", but running it directly inside the console.log returns ["", "browser"]. how can this difference be explained reasonably?
angular.module("blah", [])
.controller("navigation", function($scope, $location) {
var updatePage = function() {
var temp = $location.path().split("/");
console.log($location.path(), $location.path().split("/"), temp);
$scope.page = temp.splice(0,1)[0];
$scope.args = temp;
//console.log($scope.page);
};
$scope.changePage = function(path) {
if (path.match("^https?:")) {
window.location = path;
} else {
$location.path(path);
}
};
$scope.args = [];
$scope.$on("$locationChangeSuccess", function(event, data) {
updatePage();
});
updatePage();
});
You probably see the "issue" with a code like this:
var temp = $location.path().split("/");
$scope.page = temp.splice(0,1)[0];
...
console.log($location.path(), $location.path().split("/"), temp);
At the point of logging, temp has already been spliced. splice (unlike slice) removed the elments from the original array as well.
UPDATE:
Another possible cause is this:
Firefox (unlike Chrome) does not log the value of the object at the time of logging, but a reference to the object (Arrays are objects as well). Thus, any later modification of the Array (e.g. splicing) will affect logged object/Array as well.
If you log JSON.stringify(temp) instead, you should see the expected results.

Trouble referencing variable in Collections.where method within render function

I have run into some trouble with a piece of backbone code. The code below relates to a render function. I can retrieve all the models. My trouble arises when I try to use the "Collections.where" method at line marked number #1. As you can see, I have passed an object literal into the render function but for some reason I am unable to reference it within the customers.where method on line #1. When I give this method a literal number like 45 it works. Is there some way around this so I can pass the variable reference in?
Thanks alot
render: function(options) {
var that = this;
if (options.id) {
var customers = new Customers();
customers.fetch({
success: function (customers) {
/* #1 --> */ var musketeers = customers.where({musketeerId: options.id});
console.log(musketeers.length) //doesn't work as options.id is failing on last line
var template = _.template($('#customer-list-template').html(), {
customers: customers.models
});
that.$el.html(template);
console.log(customers.models);
}
});
} else {
var template = _.template($('#customer-list-template').html(), {});
that.$el.html(template);
}
}
Although it isn't explicitly documented, Collection#where uses strict equality (===) when searching. From the fine source code:
where: function(attrs, first) {
if (_.isEmpty(attrs)) return first ? void 0 : [];
return this[first ? 'find' : 'filter'](function(model) {
for (var key in attrs) {
if (attrs[key] !== model.get(key)) return false;
}
return true;
});
},
note the attrs[key] !== model.get(key) inside the callback function, that won't consider 10 (a probable id value) and '10' (a probable search value extracted from an <input>) to be a match. That means that:
customers.where({musketeerId: 10});
might find something whereas:
customers.where({musketeerId: '10'});
won't.
You can get around this sort of thing with parseInt:
// Way off where you extract values from the `<input>`...
options.id = parseInt($input.val(), 10);

Categories