How to find the call site that changed an object being watched - angularjs

AngularJS allows listening for changes of an object and will call the supplied callback function supplied to the $watch function. With a largish library using AngularJS like ngGrid objects are frequently changed that's being "watched".
Once the watch callback is invoked, how can one trace back the call site that caused the change to the object?
Without knowing what caused the change, and so caused the watch handler to be invoked, it is proving very difficult to debug a library like ngGrid. I'm setting breakpoints everywhere I can foresee the variable could be changed, and then trying to build a graph for the execution pipeline to follow the chain of events that lead to an object being changed.

You simply can't do that. $watch will just add a callback to check whether the object changed, to be run during digests.
I guess that's one of the main differences with frameworks like Backbone where you extend a Model object.
That being said, you might have better luck trying to $scope.$digest(); intentionally (updating the model, and firing the watchers), but it's a stretch...
Update
The problem is you're thinking there's a correlation between watches and model changes, but there simply isn't. Adding a watch just adds something to be checked when the digest loop runs.
This loop isn't triggered by changes to something on a $scope, but rather calls to $scope.$apply, or directly calling $scope.$digest.
Note that most (all?) of Angular's directives and related components call $scope.$apply on your behalf. For example, the reason why $timeout and ngClick work as expected, is because they run $scope.$apply internally after executing your callbacks.
Update II
If you're merely interested in finding the call site, could something like this help you?
$scope.foo = {
get bar () { return getting(); },
set bar (value) { setting(value); }
};
var bar;
function setting (value) {
var stack = getStack();
console.log(value, stack);
bar = value;
}
function getting () {
console.log(getStack());
}
function getStack () {
try {
throw new Error('foo');
} catch (e) {
return e.stack || e;
}
}

Well, you may have found the answer by now, but because I was looking for it, too, and could not find it so I am posting the answer here.
Let's assume your javascript variable is name, and you would like to find who changed that.
The method I have found is this:
Change name to name_
Add getter: get name() { return this.name_ ; }
Add setter: set name(value) { this.name_ = value ; }
Put breakpoint in the setter, and watch the call stack when breakpoint is triggered.
Example for a structure:
Before:
scope = {
name:'',
};
After:
scope = {
name_:'',
get name() { return this.name_; },
set name(value) {
this.name_ = value;
alert('Changed!');
},
};
I hope this will help you and others!

Related

AngularJS: Nested http call doesn't update the view

In a particular scenario, I need to call the the github api to retrieve a specific user's info. Then issue a second call to retrieve the user's repositories:
search(login: string): void {
this.user = undefined;
// first call
this.githubApi.getUser(login)
.then(user => {
this.user = user;
// second call
this.githubApi.getRepos(user.repos_url)
.then(reposResponse => {
this.repos = reposResponse.repos;
// I don't like to call this.$scope.$apply() !!;
});
});
}
The first call gets executed and the bound elements to this.user gets updated with no problem in the view.
The second call gets executed and the result is returned successfully and this.repos is set correctly. But, the bound elements on the view are not updated.
If I call this.$scope.$apply() in the very last line of the second callback, it makes the view update work but I guess this is not correct approach.
Any solution?
Well, if you are not willing to use $scope.apply();, try updating your getRepos service response code with:
setTimeout(
() => {
this.repos = reposResponse.repos;
}, 0
)
First you need to know , why Angular-Js is not updating the view.
You have used $scope.$apply(), so I'm assuming you already know , how it works and why we use it. Now , to the problem!
Sometimes when you make a callback - nested callback in particular - Angular does not update the view. Sometimes angular thinks that it does not need to update the view because of callbacks. And the watchers do not take action when the value changes of the variable that they are watching.
Then you use $scope.$apply() to run the digest cycle again (assuming you already know the digest cycle if you don't then let me know). And it makes the watchers to update the view.In your case, digest cycle is not running, that is why angular is not updating the view. If your digest cycle was running , angular would have given you error. So, it will tell angular to run digest cycle again because two-way binding is not working properly.
I don't think there is another way. But if there is a way, I would love to know that way. Also its not a bad approach. It was made for these kind of problems.

angularJS - can you watch a function call?

wondering if there is a way in angular to watch a function like you would a variable
function foo() {
console.log("foo called");
}
$scope.$watch('foo', function {
console.log("foo watched");
});
So given the syntax is correct, will "foo watched" be printed to the console, if not, is there a way to have that watch function run?
EDIT
Reformatting question to more of what I need:
Let's say I have a button and an text input, each handled by different controllers, say btnCtrl and txtCtrl
There is also a boolean global flag stored as $scope.flag which changes the validation standards for the input.
//btnCtrl
//called on button click
function onButtonClick() {
$scope.flag = !$scope.flag
}
//txtCtrl
//called on input change
function onInputChange() {
handleValidations()
}
function handleValidations() {
//set error indicator on invalid input
//valid input changes based on $scope.flag value
if($scope.flag) {
//check validation1
} else {
//check validation2
}
}
I want to check the input box validations, handleValidations when $scope.flag is changed. $scope.flag is changed frequently not by the button, but by other functions.
So, I would like to call handleValidations not when $scope.flag is changed, but when onButtonClick is called. What is the best way to do that?
My initial thought was to put some sort of $scope.$watch on onButtonClick.
You can watch the functions. Have you tried running your code? If you call foo, it should print both console statements. There is a 'gotcha' for watching functions though, mastering $watch in Angularjs
One key aspect to note here, is that if the expression is evaluated as a function, then that function needs to be idempotent. In other words, for the same set of inputs it should always return the same output. If this is not the case, Angular will assume that the data being watched has changed. In turn, this means that it will keep detecting a difference and call the listener at every iteration of the digest cycle.
With the help of the comments and other answers:
I sent a local message using $rootscope from btnCtrl and added a listener to txtCtrl that calls handleValidations
Thank you all for the help

Service Method called 5 times from Angular Controller

This one has me confounded. I have looked far and wide and am out of ideas. In my searching, I discovered that one of the common reasons for multiple function calls on load is if you have a controller defined in routes and via the ngController directive. Checked this - I do not use ngController. I also checked my routes - seem in order. There are no $watch functions that could be causing $digest issues. This function is called one time, at the bottom of the function, and the console.log is logged out 5x...EVERY TIME. I have even set a $timer function and it still calls it 5x. Have tried creating a variable to only run if it hasn't been run before but it seems like it's all happening with the getQuotas() method. Any help would be greatly appreciated!
function getQuotas ()
{
console.log('getQuotas'); //This logs out 5x
UserService.getQuotas()
.then(function(res)
{
if (res.data.success)
{
quotaData = res.data.data;
getQuotas_success();
return true;
}
else
{
getQuotas_failure();
return false;
}
}, function (err)
{
getQuotas_failure();
return false;
});
}
getQuotas(); //Function is called here.
Solved it! I'm hopeful this will help others. There was a custom attribute directive on each of 4 input fields on this page. That particular directive was using the same controller as the page itself. So the controller was getting loaded a total of 5 times. Fortunately for me, this directive is now deprecated but I would probably redo it by either creating a directive-level controller and using the 'require' attribute in the directive's return object, pointing to the page-level controller, OR just have the data that needs to get passed between the page-level controller and the directive go through a service.

Backbone events hash depends on data created during rendering

I have a Backbone view whose render method creates and caches some JavaScript objects then renders them out to SVG nodes in the DOM. So (very simplistically), I'm doing something like this:
render: function() {
// Create objects and cache them to the view...
this.someObjects = [obj1, obj2, ...];
// Render objects...
this.someObjects.forEach(function(obj) {
obj.render(); // Each object knows how to render itself...
})
return this;
}
My view events hash is returned from a function, and contains event handlers which close over certain variables which depend on the objects cached when rendering. So (again simplistically, just for illustration purposes), I'm doing something like this:
events: function() {
var getRenderedObjects = function() {
return this.someObjects;
};
var renderedObjects = getRenderedObjects();
return {
'click #someDiv .someClass' : function() {
console.log(renderedObjects);
}
}
}
The problem is that Backbone sets up the bindings for the events hash before the view initialization function runs, which is where I need to do an explicit render. So this.someObjects will be undefined. Before moving to Backbone all my event handlers were defined and bound after rendering the view, which seemed natural anyway. I have found a workaround, which is to set the events hash manually on initialization, after rendering, and then call delegateEvents() to ensure they're bound. This works, although I found that within the handlers this no longer refers to the view object, but rather to the global window object again. It all seems rather cumbersome and a bit hackish, however.
I guess I could leave renderedObjects as null and set it conditionally in the handler which requires it (so if it's not set, set it, else do nothing). That seems ugly, though. Is there a better way?

Watch for dom element in AngularJS

I'm looking for a pure angularJS way to call a controller method once a particular dom element is rendered. I'm implementing the scenario of a back button tap, so I need to scroll to a particular element once it is rendered. I'm using http://mobileangularui.com/docs/#scrollable.
Update: how my controller looks like:
$scope.item_ready=function(){
return document.getElementById($scope.item_dom_id).length;
};
$scope.$watch('item_ready', function(new_value, old_value, scope){
//run once on page load, and angular.element() is empty as the element is not yet rendered
});
Thanks
One hack that you could do and I emphasize hack here but sometimes it's just what you need is watch the DOM for changes and execute a function when the DOM hasn't changed for 500ms which is accepted as a fair value to say that the DOM has loaded. A code for this would look like the following:
// HACK: run this when the dom hasn't changed for 500ms logic
var broadcast = function () {};
if (document.addEventListener) {
document.addEventListener("DOMSubtreeModified", function (e) {
//If less than 500 milliseconds have passed, the previous broadcast will be cleared.
clearTimeout(broadcast)
broadcast = $window.setTimeout(function () {
//This will only fire after 500 ms have passed with no changes
// run your code here
}, 10)
});
}
Read this post Calling a function when ng-repeat has finished
But don't look at the accepted answer, use the 3rd answer down by #Josep by using a filter to iterate through all your repeat items and call the function once the $last property returns true.
However instead of using $emit, run your function...This way you don't have to rely on $watch. Have used it and works like a charm...

Resources