I have a large project where one method attached to the scope has a console.log in it.
$scope.someFunctionAttachedToView = function() {
console.log("this method is being triggered");
return {
'width': '500px'
}
}
In this project, I have lots of listeners that listen for browser window size, and mouse clicks etc. and I noticed that sometimes, even if I don't think I'm making an action that would trigger a digest cycle, I see the console.log message above appearing in the browser console intermittently.
I am trying to track down what action could be possibly triggering the digest cycle to apply, as it is not intended behavior.
How would you track this? Is there something I can console.log in the Angular object that would tell me this?
If you're working with chrome/firefox you could put a debugger in your code like this:
$scope.someFunctionAttachedToView = function() {
debugger;
console.log("this method is being triggered");
return {
'width': '500px'
}
}
When this function triggers your javascript will be frozen and you can look in the stack trace which function called it. (In chrome you might have to enable async because digests often mess up the call stack)
Related
I'm using angular, and in an angularUI modal window I want to show the Drop In form from Braintree to get a payment method. Thus, I create the usual form (partial.html):
<form id="creditCard" >
<div id="dropin"></div>
<button type="submit" id="btnPay" >Pay</button>
</form>
and then I show the modal with this:
var modalInstance = $modal.open({
templateUrl: 'partial.html',
controller: 'ModalController'
});
Where ModalController contains the call to the Braintree setup:
braintree.setup($scope.clientToken, 'dropin', {
container: 'dropin',
onPaymentMethodReceived: function (result) {
$scope.$apply(function() {
$scope.success = true;
// Do something else with result
});
}
});
This will show the Drop In form from braintree nicely (the setup generates the form) and accept the credit card and expiration date, all working fine so far.
The problem is, each time I call the modal, the ModalController is executed, and thus the braintree.setup() is also executed. Then, when I enter the credit card number and the expiration date and hit pay, the onPaymentMethodReceived() event is triggered once per setup execution! That is, if the first time I call the modal it will trigger the event once, the second time it will trigger it twice, and so on. Like if each time I call setup, a new hook to the event is created.
Any idea on how to avoid this? Is there a way to "unbind" the onPaymentMethodReceived() event handler? I do need to call the setup several times since each time I call the modal, the clientToken may have changed.
Thanks for any help or pointer to help.
Calling braintree.setup multiple times in angular seems unavoidable, either for the asker's reasons, or simply because setup is called in a controller that may be instantiated multiple times in a browsing session – like a cart or checkout controller.
You can do something like this:
$rootScope.success = false;
braintree.setup($scope.clientToken, 'dropin', {
container: 'dropin',
onPaymentMethodReceived: function (result) {
if(!$rootScope.success) {
$scope.$apply(function() {
$rootScope.success = true;
// Do something else with result
});
}
}
});
I found I wasn't able to avoid having the callback fire multiple times (the number of times seems to explode each time I revisit the view - yikes), but I could test whether I had performed my actions in response to the callback. Since the $scope will be destroyed if I leave the view, $scope.success is effectively reset when I need it to be. Because each new controller will have its own $scope, setting a success flag on the $scope may only halt additional executions on that $scope (which seems to still be available to the callback, even if the controller has been "destroyed"), so I found that using $rootScope meant only one execution total, even if I re-instantiated the controller multiple times. Setting $rootScope.success = false in the controller means that once the controller is loaded, the callback will succeed anew – once.
I think it's handled by the API since then with teardown:
In certain scenarios you may need to remove your braintree.js integration. This is common in single page applications, modal flows, and other situations where state management is a key factor. [...]
Invoking teardown will clean up any DOM nodes, event handlers, popups and/or iframes that have been created by the integration.
https://developers.braintreepayments.com/guides/client-sdk/javascript/v2#teardown
(I haven't tried it yet)
The link given by Arpad Tamas does not contain the info anymore. So I am posting the info given by BrainTree for posterity ;) Especially since it took me a few tries to find it with a Google search.
In certain scenarios you may need to remove your Braintree.js integration. This is common in single page applications, modal flows, and other situations where state management is a key factor. When calling braintree.setup, you can attach a callback to onReady which will provide an object containing a teardown method.
Invoking teardown will clean up any DOM nodes, event handlers, popups and/or iframes that have been created by the integration. Additionally, teardown accepts a callback which you can use to know when it is safe to proceed.
var checkout;
braintree.setup('CLIENT_TOKEN_FROM_SERVER', 'dropin', {
onReady: function (integration) {
checkout = integration;
}
});
// When you are ready to tear down your integration
checkout.teardown(function () {
checkout = null;
// braintree.setup can safely be run again!
});
You can only invoke teardown once per .setup call. If you happen to call this method while another teardown is in progress, you'll receive an error stating Cannot call teardown while in progress. Once completed, subsequent calls to teardown will throw an error with this message: Cannot teardown integration more than once.
I've wrapped this code in a function that I call each time the related checkout ionic view is entered.
$scope.$on('$ionicView.enter', function() {
ctrl.setBraintree(CLIENT_TOKEN_FROM_SERVER);
});
var checkout;
ctrl.setBrainTree = function (token) {
braintree.setup(token, "dropin", {
container: "dropin-container",
onReady: function (integration) {
checkout = integration;
$scope.$emit('BTReady');
},
onPaymentMethodReceived: function(result) {
...
},
onError: function(type) {
...
}
});
// Prevents a call to checkout when entering the view for the first time (not initialized yet).
if (checkout) {
// When you are ready to tear down your integration
checkout.teardown(function () {
checkout = null; // braintree.setup can safely be run again!
});
}
};
I need to test if specific methods are called when user scrolls the window to a certain point. In my source code I have windows listener attached, something like:
$(window).on("scroll.singleJob",function(e)
{
// code here, check if the window is scrolled past certain point etc. and then I need to call this method
LozengesPanel.makeFixed();
}
Now, in my Jasmine test I'm trying to confirm that the method is being called when the window is scrolled to a certain point. So I set up the test:
describe("SingleJob page", function() {
beforeEach(function() {
loadFixtures('my_fixture.html');
});
it("panel sticks to top when page scrolled down", function() {
spyOn(mycompany.singleJobTestable.LozengesPanel, "makeFixed");
window.scroll(0,1000);
expect(mycompany.singleJobTestable.LozengesPanel.makeFixed).toHaveBeenCalled();
});
});
But the test fails, all I get is Expected spy makeFixed to have been called.
How can I trigger window scroll so I can test methods inside of this callback?
EDIT:
Finally it all makes sense.. It seems that scroll event was put in a tasks queue only to be executed after the current thread finishes. Adding $(window).trigger("scroll"); did the trick. I posted short blog post about it that explains the issue http://spirytoos.blogspot.com.au/2014/02/testing-windowscroll-with-qunitjasmine.html
EDIT: This answer does not satisfy the question. See the comments for the reason.
Actually, it looks like you are triggering the scroll event from your Jasmine spec. I tried very similar code, which I include below. However, my expect still fails, like yours (I'm still getting familiar with Jasmine, so I can't explain with certainty why that is).
var fun = {
scrollEventCallback: function() {
console.log('scroll event triggered');
}
};
$(window).on('scroll', fun.scrollEventCallback);
describe("A test suite", function() {
it("should trigger f", function() {
spyOn(fun, "scrollEventCallback");
$(window).trigger('scroll'); // my callback function is executed because it logs to the console
expect(fun.scrollEventCallback).toHaveBeenCalled(); // this fails anyway
});
});
Maybe your window.scroll(0, 1000) is not actually pushing the viewport low enough to trigger your Lozenges.makeFixed() call. That would be the case if the page (your fixture, I think) wasn't long and it didn't actually have anywhere to scroll.
Also, I got code similar to your provided code to work. The expect(...) succeeds. It is pasted below.
var LozengesPanel = {
makeFixed: function() {
console.log('makeFixed was called with its original function definition');
}
};
$(window).on("scroll.singleJob",function(e) {
LozengesPanel.makeFixed();
});
describe("A test suite", function() {
it("should trigger callback", function() {
spyOn(LozengesPanel, "makeFixed");
$(window).trigger('scroll'); // nothing is logged to the console
expect(LozengesPanel.makeFixed).toHaveBeenCalled(); // succeeds
});
});
The strangest thing just happened to me. I have some javascript that appears to be being executed in the wrong order. This is freaking me out! Observe:
the code
handleKeyDown: function (e) {
console.log("handleKeyDown");
var key = e.which;
var text = this.ui.$input.val();
if (_.isFunction(this[key])) {
// call the appropriate handler method
this[key](text, e);
console.log("before announceEdits");
this.announceEdits();
}
if (key === ENTER || key === ESC) {
console.log("fired field:key:down");
this.trigger("field:key:down", { editable: this, restore: (key === ESC) });
}
},
announceEdits: function () {
console.log("announceEdits");
var edits = this.getEdits();
console.log("edits: %o", edits.data);
console.log("fired field:edits");
this.trigger("field:edits", edits);
},
/* gather all the existing taggies */
getEdits: function () {
var data = this.$taggies().map(function (index, taggy) {
return $(taggy).data("value");
}).toArray();
var edits = {
attribute: "tags",
data: data
};
return edits;
},
When I run this code, the functions appear to be being executed out of order. This is the output in firefox's console of the above code:
the output
Notice that we get before announceEdits long before we get announceEdits, which is the first line in the annouceEdits function... my understanding of the world leads me to believe this is wrong.
what I've done
Now, I have considered the following:
The console statements could be being buffered or some such, causing them to appear out of order.
This could have something to do with the way MarionetteJS handles events.
Believing that this might be evented weirdness, I tried removing the calls to trigger (just by commenting out the lines that trigger the events). After removing the triggers, the log statements still appear out of order. So it doesn't seem to be cause by MarionetteJS's implementation of events (which is to say, BackboneJS's implementation of events ^o^//).
I'm also lead to believe that this isn't a log statement buffer issue, because the events are themselves handled out of order (i.e. the data I expect to have after the handling of the first event is not in order by the time the second event is handled). This causes the code to "not work" in the way I would like (however, see below).
In my explorations, I've tried to narrow things down a bit. I modified the code thusly, in order to simplify the code:
if (_.isFunction(this[key])) {
// call the appropriate handler method
this[key](text, e);
console.log("before announceEdits");
console.log("announceEdits");
var edits = this.getEdits();
console.log("edits: %o", edits.data);
console.log("fired field:edits");
this.trigger("field:edits", edits);
}
This way I am not descending into a subroutine. Running this code, the console statements appear in the right order. What's more interesting is that, in this case, the events are also fired and handled in the order I expect! This code works, but the one with the subroutine doesn't.
I tried to create a fiddle of this code, but jsfiddle doesn't appear to like Backbone (I tried including the library as an external library, but that didn't seem to work). I did create this fiddle, just to reassure myself that somewhere in the world there is still a rock of normality.
update: I changed the fiddle so that the main function is itself a handler. Everything still works fine in the fiddle.
how I thought the world worked
Functions create stack-frames that execute to completion. If a function calls another function, a new stack-frame is pushed to the stack. If a function triggers an event, it creates a message, which is pushed onto a message queue. Whenever (and I'm unclear on this) the stack is empty, the next message in the queue is popped off, and all handlers for this message are then invoked.
In this model, without question, the events should happen in the order that I expect (the order from the second listing). We should first descend into announceEdits, push the field:edits message to the queue, and then pop back out and push the field:key:down message. Then the handlers for field:edits should run, and finally those for field:key:down.
the question
Is there anything that could be causing these functions to be being executed out of order? Or more likely: is there anything that could be causing these functions to appear to be being executed out of order?
If this is a problem with my understanding: where did I go wrong?
If this ends up being something that was caused by a typo, please bear in mind that we are all programmers, and that we have all torn out hair and bellowed at shadows at times.
lesson
Sleep on it. In the heat of the moment, every bug is a mysterious force from another world. Step back from the problem, start from the beginning. Everything will become clear.
I came in this morning, put a debugger at the beginning of handleKeyDown and immediately saw what was wrong. It isn't in the code I gave above, naturally, and it isn't a problem with how javascript works (clearly!). I had wrapped announceEdits in a debounce earlier, to relieve a head-ache I had been having.
Good news is, my understanding of how event handling works does not appear to be in question.
The following must be the most common error Angular.js developers have to troubleshoot at some point or another.
Error: $apply already in progress
Sometimes, a $scope.$apply call wasn't required after all, and removing it fixes the issue. Some other times, it might be needed, and so developers resort to the following pattern.
if(!$scope.$$phase) {
$scope.$apply();
}
My question is, why doesn't Angular merely check for $scope.$$phase and return without doing anything when a $digest loop is in invoked, or when $scope.$apply is called, rather than complain and throw an error so insidiously hard to track down.
Couldn't Angular just check for $$phase itself at the entry points of any methods which trigger a $digest and save the consumers from having to go through the trouble?
Are there any implications in leaving misplaced $scope.$apply calls around, other than unwanted watches firing, which the check for $$phase would prevent?
Why isn't a$$phase check the default, rather than an oddity?
Background
I'm upgrading Angular from 1.2.1 to 1.2.6 and that exception is being thrown when the page loads, so I have no idea where it's originating from. Hence the question, why is it that we even have to track down this obscure sort of bug with no idea of where in our code to look for it?
If you ever get this error, then you're using $scope.$apply() in the wrong place. I think the reasoning behind this error is as follows: if you're calling $apply in the wrong place, you probably don't fully understand the reason it exists, and you may have missed places where it actually should be called.
If such lapses are allowed to go by silently (by always testing $scope.$$phase first, for example), it would encourage sloppy usage, and people might get holes in their "$apply coverage". That kind of bug (where a $scope change is not reflected in the view) is a lot harder to track down than this error message, which you should be able to debug by examining the stack-trace (see below).
Why does $apply exist?
The purpose of $apply is to enable automated two-way data binding between $scope and view, one of the main features of Angular. This feature requires that all code that may potentially contain a $scope modification (so, basically every piece of user-code) is run below an $apply call in the call-stack. Doing this properly is actually not that complicated, but I think it's not documented very well.
The main 'problem' is that Javascript can have any number of active call-stacks, and Angular is not automatically notified about new ones. A new call-stack is created whenever an asynchronous callback is triggered, e.g., from a click, a timeout, a file-access, a network response, and so on. It's very common to want to modify the $scope inside such a callback function. If the corresponding event was provided by Angular itself, everything will just work. But there are times when you'll want to subscribe to 'outside events'. Google Maps events, for example:
function Controller($scope) {
$scope.coordinates = [];
//...
var map = new google.maps.Map(mapElement, mapOptions);
google.maps.event.addDomListener(map, 'dblclick', function (mouseEvent) {
$scope.coordinates.push(mouseEvent.latLng);
});
}
http://jsfiddle.net/mhelvens/XLPY9/1/
A double-click on the map of this example will not trigger an update to the view because the coordinates are added to $scope.coordinates from an '$applyless' call-stack. In other words, Angular does not know about Google Maps events.
How and where to use $apply?
We can inform Angular about the event by using $scope.$apply():
function Controller($scope) {
//...
google.maps.event.addDomListener(map, 'dblclick', function (mouseEvent) {
$scope.$apply(function () {
$scope.coordinates.push(mouseEvent.latLng);
});
});
}
http://jsfiddle.net/mhelvens/XLPY9/2/
The rule is to do this first thing inside every callback function for an outside event. The "$apply already in progress" error is an indication that you're not following this rule. If you have to handle Google Maps events often, it makes sense to wrap this boilerplate code in a service:
app.factory('onGoogleMapsEvent', function ($rootScope) {
return function (element, event, callback) {
google.maps.event.addDomListener(element, event, function (e) {
$rootScope.$apply(function () { callback(e); });
});
};
});
function Controller($scope, onGoogleMapsEvent) {
//...
onGoogleMapsEvent(map, 'dblclick', function (mouseEvent) {
$scope.coordinates.push(mouseEvent.latLng);
});
}
http://jsfiddle.net/mhelvens/XLPY9/3/
The onGoogleMapsEvent events are now Angular-aware, or 'inside events' if you will (I'm totally making these terms up, by the way). This makes your code more readable and allows you to forget about $apply in your day-to-day programming; just call the wrapper instead of the original event subscriber.
For a number of events, Angular has already done this for us. With the $timeout service, for example.
Debugging the "$apply already in progress" error
So let's say I use the wrapper, but, absentminded as I am, I still call $apply manually:
onGoogleMapsEvent(map, 'dblclick', function (mouseEvent) {
$scope.$apply(function () { // Error: $apply already in progress
$scope.coordinates.push(mouseEvent.latLng);
});
});
http://jsfiddle.net/mhelvens/XLPY9/4/
This $apply call does not follow our placement rule (onGoogleMapsEvent events are not outside events, after all; we granted them 'insideness'). If you double-click on the map, you'll see the error appear in the logs, together with a stack-trace:
Error: [$rootScope:inprog] $apply already in progress
...
at Scope.$apply (.../angular.js:11675:11)
at http://fiddle.jshell.net/mhelvens/XLPY9/4/show/:50:16
...
at Scope.$apply (.../angular.js:11676:23)
at dl.ondblclick (http://fiddle.jshell.net/mhelvens/XLPY9/4/show/:33:24)
...
I've left only the relevant lines: the ones that refer to Scope.$apply. When you get this error message, you should always find two Scope.$apply calls on the stack (or Scope.$digest calls, which serve a similar function; $digest is called by $apply).
The topmost call indicates where we got the error. The other one indicates why. Go ahead and run the Fiddle with your Javascript console opened up. In this case we can conclude: "oh, this was an 'inside event' subscriber, I can just remove my manual $apply call". But in other situations you may find out that the call lower down on the stack is also not positioned properly.
And that's precisely why I believe the error is useful. Ideally you'd only get an error when you neglect to call $apply altogether. But if Angular could do that, there would be no need to call the function manually in the first place.
I'm relatively new to javascript and having a couple of issues that I'm hoping you can help with. I'm utilizing the fabric library with canvas to create an animation of sorts.
I have a function that is called when the relevant button is pressed:
//for starting the animation
startSim.onclick = function(){
//obtain speed value from the control panel.
var speed = 10000 / (new Number(speedControl.value));
//stuff
//animation process
object.animate('top', '+='+canvas.height,{
duration: speed,
abort: function(){
var returnedBool;
//Pause is pressed, simulation pauses
if(pause){
//stuff
}
//restart required
if(restart){
//stuff
}
//option here to save data?
return returnedBool;
},//end abort callback
onChange: canvas.renderAll.bind(canvas),
onComplete: function(){
//stuff
}
});//end animate callback
};
What I'd like to do is call certain other functions while the above animation is active.
I know how I'd like to do this using Java or C++ but I can't seem to call the function.
I have a display panel that should output various information - as the animation completes variables will change and I'd like them to be output on the screen and be updated as they change.
I'm testing with a simple function:
function updateInfoPanel(testSpeed){
var test = document.getElementById('currentSpeed');
test.innerHTML = test.innerHTML + testSpeed;
}
I think I need to call this in the main animate loop, if I call it before it's fine but obviously only displays the variable once. Could someone tell me where I'm going wrong please?
I also have a basic collision detection function that works but not within the animate loop and I think these problems are linked.
Many thanks for any help.
I removed the unnecessary parts of the code to save space.
So I've just tried to use a function as the result of onChange:
onChange: function(){
canvas.renderAll.bind(canvas);
updateInfoPanel(speed);
},
This doesn't present syntax errors but the animation no lingers runs although the speed variable does update in the info panel very quickly. :(
You need to call renderAll(), bind is actually returning a function to be used as a handler. Just try this code:
onChange: function(){
canvas.renderAll();//.bind(canvas);
updateInfoPanel(speed);
},
And let me know if it helped?