Ionic: $timeout action is being executed even if it is cancelled - javascript

Good afternoon,
I've been struggling with an unfamiliar problem. But first let me explain what I'm trying to achieve.
Purpose
What I'm trying to do is, setting a timer on each view, so if the internet connection is slow the timer calls a function which displays an ionicPopup with the question: "Would you like to refresh the view?" combined with a refresh button. When the button is clicked, the view will refresh. If the view loads in time (I've set the timer on 10000 milliseconds), the $timeout.cancel action is being called and the timer stops.
If that would be all it would be working fine at the moment.
Problem
If I navigate through my main pages rapidly, it looks like the timers aren't stop and the "Refresh popup" pops up on another page. That only happens when you click through at high speed. It looks like a few popup pop up at once beneath each other. I don't know what might be the problem, but I guess that the $timeout.cancel isn't being called if you navigate through the pages to quickly.
Code
Service
.factory('Timer', function ($ionicPopup, $state, $timeout) {
return {
start: start
};
function start() {
return $timeout(showPopup, 10000);
}
function showPopup() {
$ionicPopup.show({
title: 'Slow internetconnection',
template: 'Click refresh to try again.',
buttons: [
{
text: 'Refresh',
onTap: function (e) {
$state.reload()
}
}
]
})
}
})
Controller
.controller('HomeCtrl', function ($scope, Request, Timer, $timeout) {
var timer = Timer.start();
Request.execute().finally(function () {
$timeout.cancel(timer);
})
$scope.$on('$destroy', function () {
$timeout.cancel(timer);
});
})
I hope my problem is understandable.

The key thing to note here is:
If I navigate through my main pages rapidly, it looks like the timers
aren't stop and the "Refresh popup" pops up on another page.
As you navigate through, your ionic views are created/destroyed, and therefore you lose references to timer objects.
So, in controllers where you are using timers, you must invalidate the timer before you leave that page. This is because timers are wrappers around JavaScript's setTimeout and setInterval, and JavaScript has method level scope that passes the method as a parameter to setInterval which will hold a reference to it, while your controller gets destroyed. Therefore, when you change view, although your controller instance may get removed, the timer does not.
Add $ionicView.afterLeave callbacks in your controllers.
$scope.$on("$ionicView.afterLeave", function(event, data){
$timeout.cancel(timer);
});
This will cancel the timer for a particular controller, after that controller's view is dismissed.

Related

Unexpected window.confirm behaviour while hooking into angular-ui-router $transitions API

I've got this listener setup on a form of mine that checks for a state transition to occur via angular router. When the listener is tripped it checks if the form is dirty, if it is it throws a window.confirm alert up saying the user may have unsaved changes.
All of that looks like this
this.setListener('form.dirty-check', this.setExitCheck);
setListener = (el, cb) => {
if ($(el).length) {
cb();
} else {
setTimeout(() => {
this.setListener(el, cb);
}, 500);
}
};
setExitCheck = () => {
this.$transitions.onStart({}, () => {
if ($('#compForm').hasClass('ng-dirty')) {
if (window.confirm('You may have unsaved changes! Press ok to continue, or press cancel to go back and save your work.') === false) {
return false;
} else {
return true;
}
}
});
};
This code is working pretty well, except for a singular bit of unexpected behaviour.
For some reason, when I hit, "Ok" to leave the page the transition will fire off just fine, but if I go back to the page and try it again, I now have to hit okay twice, and get two window.confirm alerts. If I go back a third time to try, I get three window.confirm alerts, and have to hit Ok on all three of them. I tried this up to the point of receiving 10 alerts, and have to press ok 10 times.
Once I refresh the page though, it seems to reset, and I start it all over again. Works right away, then takes two Ok's, then three, and so on.
Does anyone know what might be going on causing this incremental behaviour?
ui-router won't clear listeners automatically, so you have to clear it manually.
and $transitions.onStart returns a function which will destroy the listener's hook when it's called. documentation here(the last line).
the syntax is the same as deregister events of $rootScope, refer How can I unregister a broadcast event to rootscope in AngularJS?
$scope.onStartHandler = this.$transitions.onStart(...);
$scope.$on('destroy', function() {
$scope.onStartHandler();
});

Ionic 2 Popover dismiss

I have a popover, that takes me to another page, where I pop back to the root page (popToRoot), reload the data/dom on an event and then dismiss the popup in the promise when the json data comes back from the server. It all works fine if I have a large timeout on the dismiss.
dismissPopup() {
if (this.popover) {
let that = this;
setTimeout(function () {
that.popover.dismiss();
}, 500);
}
}
If I make the timeout too low, say 100ms, it does not dismiss because the dom is still loading.
However, I don't think having a timeout is probably the best practice. What happens if someone has a slow devise, and the time is not enough?
Can anyone please make any suggestions? Should I detect when the dom has loaded, and then call dismiss? How do I check if the dom had loaded?
Thanks
Instead of using a timeout, you can use Events. By doing that, you can publish and event when the data comes back from the server (and everything is ready) and subscribe to that event to know when you need to dismiss the popup.
import { Events } from 'ionic-angular';
constructor(public events: Events) {}
// first page (publish an event when data is ready)
events.publish('loading:finished', data);
// second page (listen for the loading finished event)
events.subscribe('loading:finished', (eventData) => {
// eventData is an array of parameters, so grab our first and only arg
console.log('Data:', eventData[0]);
});
The popover can also be dismissed from within the popover's view by calling the dismiss() method on the ViewController
constructor(public navParams:NavParams,public navCtrl:NavController,public viewController:ViewController) {
console.log('Hello PopOverComponent Component');
}
blah()
{
//do something
this.viewController.dismiss();
}

AngularJS - How to see what is triggering $scope.$apply()

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)

AngularJS / IonicFramework prevent multiple API calls on toggle

I've read this and am trying to implement similar functionality
"If you’re using toggle buttons, e.g.: to turn something on and off and it hits an API endpoint, be sure to wrap that interaction in a timer so you’re not calling that endpoint multiple times if the user decides to tap the control multiple times in quick succession. Wait a second, then hit the endpoint."
I have a settings page with a series of toggle switches. Rather than do an API call at the change of each and every toggle I want to delay the API call that passes the value of the scope to my server. I want to delay it by X seconds. However, by doing this
$scope.settingsChange = function () {
$timeout(function () {
//save_notifications
console.log('called');
// Trigger API call
}, 3000);
};
All that's happening its just delaying the call by 3000ms. I still get the console log appear 3 times if I toggle something 3 times.
I'm fully aware this is because I've implemented a timeout function. I'm not sure what to implement to get the functionality the poster alludes to in my quote above.
Any ideas much appreciated.
Thanks,
Note: my template looks like this
<ion-toggle ng-repeat="item in settingsData"
ng-model="item.checked"
ng-checked="item.checked"
ng-change="settingsChange()">
{{ item.text }}
</ion-toggle>
once you initiate the API call, block any other calls from being made.
$scope.settingsChange = function () {
if ($scope.processingSettingChange) return;
$scope.processingSettingChange = true;
$timeout(function () {
//save_notifications
console.log('called');
// Trigger API call
$scope.processingSettingChange = false;
}, 3000);
};
you might also want to disable the UI while the API is processing the request. You should be able to using the same $scope variable and ng-disabled

Braintree multiple setup calls yield in multiple onPaymentMethodReceived events

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!
});
}
};

Categories