How to prevent default browser back button functionality in AngularJS? - angularjs

I'm making a mobile app and it has a back button in the navbar of the app. The button is used for navigating one level up in the application. Not all levels of the applications are written in the url, some of them are skipped so i cannot rely on the browser back button because not everything is written in the history. So what i want to do is to prevent the default event that happens when you click the back button (even the url change because i have a function that's manually rewriting the url when you click on some parts of the app) and i want the browser's back button to be tied to my custom function.
I'm thinking of adding a false state in the history with history.pushstate so when i'm pressing back i'll be going to the false state, but unfortunately you can't have two states with a same name.
Do you guys know any solution for this?
Thanks

We want to prevent that users can use the back button when logged off or when the token is not valid anymore.
// Prevent to use the back button.
$scope.$on('$locationChangeStart', function(event) {
if (!$scope.isAuthenticated) {
event.preventDefault();
}
});
Where the if is you can implement your url rewrite function.
Hope this helps

try this:
$rootScope.$on("$routeChangeStart", function(event, next, current) {
if(current && current.params === "example") {
$location.path("/defaultPage");
}
});

The following code should do the trick:
var allowNav = false;
var checkNav = false;
$rootScope.$on('$stateChangeSuccess', function (event, toState, toStateParams, fromState, fromStateParams) {
allowNav = checkNav;
checkNav = true;
});
$rootScope.$on('$locationChangeStart', function (event, next, current) {
// Prevent the browser default action (Going back)
if (checkNav) {
if (!allowNav) {
event.preventDefault();
}
else {
allowNav = false;
}
}
});

Related

how to prevent ui-bootstrap pagination control from changing the url route

Within my application I have two views search and masternameserch these views are defined in app.js as:
$stateProvider
.state('search', {
url: '/',
controller: 'searchController',
controllerAs: 'search',
templateUrl: '/app/views/search.html'
})
.state('searchmasterName', {
url: '/searchmastername',
controller: 'searchMasterNameController',
controllerAs: 'masternameSearch',
templateUrl: '/app/views/searchmastername.html'
})
Within my views I have the ui-bootstrap pagination control setup as
<div class="text-center">
<uib-pagination ng-show="masternameSearch.pgTotalItems > 10" total-items="masternameSearch.pgTotalItems" ng-click="masternameSearch.pageChanged(); $event.stopPropagation();" ng-model="masternameSearch.pgCurrentPage" class="pagination-md" max-size="masternameSearch.pgMaxSize" boundary-links="true"></uib-pagination>
</div>
And within the controller I have the pageChanged() function setup as follows:
vm.pageChanged = function () {
var pageRequest = buildMasterNameSearchRequest(); //get cached request
pageRequest.pageFrom = vm.pgCurrentPage; //set page number to request
var currentPath = $state.current.name;
searchService.postSearchRequest(pageRequest).then(renderResults, onError);
$state.go('searchmastername',
{},
{
notify: false,
location: false,
inherit: false,
reload: false
});
$timeout(function () { location.hash = '#top' }, 1000);
}
The question I have is whenever clicking on the pagination control the underlying URL is always the root url. Therefore when I click on a pagination button the search executes but I am navigated back to the default main view which is wrong. As you can see from the code I first tried in the directive itself to change the onchange to an ng-click event and tried to stop propagation to stop the redirect through $event. This did not work. Second thing I tried was to call a state transition / state.go() in the pageChanged() function where I basically reload the view. This however does not work as it throws an error that the state cannot be found. Sadly this actually prevents the page from reloading or navigating to the main page so the error actually makes the page work as the end user might expect, but with errors generated around the missing state I know this isn't right.
Update: The error was generated from a type searchmastername and not searchmasterName.
Making this change fixed the error but still causes the site to redirect to the default view to load.
Can anyone provide an idea or ways to get the pagination control to not cause a navigation event by redirecting me to the default main view when ever clicked?
-cheers
After doing some more studying of this problem I thought the issue could be addressed by listening to the $rootScope for the stateChangeStart event and from there add a preventDefault() function to stop the navigation from occurring in the route which I did.
So the code now looks like this on the pageChanged() function
vm.pageChanged = function () {
var pageRequest = buildMasterNameSearchRequest(); //get cached request
pageRequest.pageFrom = vm.pgCurrentPage; //set page number to request
searchService.postSearchRequest(pageRequest).then(renderResults, onError);
$rootScope.$on('$stateChangeStart',
function(event) {
event.preventDefault();
});
$timeout(function () { location.hash = '#top' }, 1000);
}
While useful, using preventDefault() stops all further interaction with the page. Links would no longer work etc. I thought I had fixed but instead I had just stopped the event so the timeout never got called which was the culprit.
The real issue and I did it to my self was adding the timeout and using the location route #.
$timeout(function () { location.hash = '#top' }, 1000);
Using this function was actually (as designed) changing my route in my URL and navigating me to the main view. I needed to have the ability to scroll to the top so I changed the timeout function to look like this
vm.pageChanged = function () {
vm.showPager = false;
var pageRequest = buildMasterNameSearchRequest(); //get cached request
pageRequest.pageFrom = vm.pgCurrentPage; //set page number to request
searchService.postSearchRequest(pageRequest).then(renderResults, onError);
window.scrollTo(0, 0);
}
Using window.scroll accomplished the same scrolling effect but did not change the route.

$scope.$on('$stateChangeStart') and $modal dialog

I have an AngularJs application that is detecting the change of the state (using ui.router) to present the user with the option to save unsaved changes. Now I'm doing this with a confirm dialog:
$scope.$on('$stateChangeStart', () => {
if (self.changed && confirm('There are unsaved changes. Do you want to save them?'))
this.save();
});
I wanted to change it to using the $modal dialog from the bootstrap ui library. The problem I have is that as the $modal.open() call returns inmediatelly (being asynchronous) the state changes before opening the dialog and it's never opened.
$scope.$on('$stateChangeStart', () => {
if (self.changed)
this.$dialog.open({...}).result.then(()=>{
this.save();
});
});
Is there a way to overcome this problem or am I stuck to using the plain javascript confirm dialog?
This is how I solved the problem. In my application, I am using an AppCtrl (parent) to handle navigation, dirty state etc.
function AppCtrl($rootScope, events, modalDialog) {
var vm = this,
handlingUnsavedChanges = false;
function isDirty() {
return $rootScope.$broadcast(events.CAN_DEACTIVATE).defaultPrevented;
}
function onStateChangeStart(event, toState, toParams) {
if (handlingUnsavedChanges) {
// if the dirty state has already been checked then continue with the state change
return;
}
// check for dirty state
if (isDirty()) {
// cancel navigation
event.preventDefault();
modalDialog
.confirmNavigation()
.then(function () {
// ignore changes
handlingUnsavedChanges = true;
$state.go(toState.name, toParams);
});
}
// Else let the state change
}
$rootScope.$on('$stateChangeStart', onStateChangeStart);
}
<div ng-controller="AppCtrl as app">
<div ui-view />
</div>
Then you can add an event handler for CAN_DEACTIVATE event in your route controller to check for dirty state, for example
function UserDetailCtrl($scope, events) {
function isDirty() {
// Your logic, return a boolean
}
function canDeactivate(e) {
if (isDirty()) {
e.preventDefault();
}
}
$scope.$on(events.CAN_DEACTIVATE, canDeactivate);
}
You can do it perfectly fine with ui-router and settings in run section of your app.
The crucial part is basically watching the $rootScope.$on('$stateChangeStart') event.
I present the whole solution with the plunker: http://plnkr.co/edit/RRWvvy?p=preview The main part is in scipt.js, and it goes as follows:
routerApp.run(function($rootScope, $uibModal, $state) {
$rootScope.modal = false; // this has to be set so the modal is not loaded constantly, and skips the 1st application entrance
$rootScope.$on('$stateChangeStart',
function(event, toState, toParams, fromState, fromParams) {
if ($rootScope.modal) {
event.preventDefault(); //this stops the transition
$rootScope.modal = false;
var modalInstance = $uibModal.open({
templateUrl: 'modal.html'
});
modalInstance.result.then(function(selectedItem) {
console.log('changing state to:'+ toState.name);
$state.go(toState, {}, {reload:true});
}, function() {
console.log('going back to state:'+ fromState.name);
$state.go(fromState, {}, {reload:true});
});
} else {
$rootScope.modal = true;
}
});
});
You should listen the $locationChangeStart event and if you have unsaved changes then do event.prventDefault() and continue on with your logic for the confirmation dialog with ui-bootstrap modal. The thing is that the order of the events is slightly changed in the newer angular versions and the $stateChangeStart happens a little too late to do your logic there. If you get stuck I can provide you with working example. For now, here is sample code:
$scope.$on('$locationChangeStart', () => {
if (self.changed) {
event.preventDefault();
this.$dialog.open({...}).result.then(()=>{
this.save();
// your logic for the redirection if needed after the save
});
}
});
If you want to work with state change event , it would be better to apply the event on app and use $modal service. You can find a detailed explanation of this here
http://brewhouse.io/blog/2014/12/09/authentication-made-simple-in-single-page-angularjs-applications.html
However this gives an example that works for each state change whenever state changes but you can make it working for the single or specified states like this :
app.run(function ($rootScope) {
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
if(toState.name =="desiredState") {
event.preventDefault();
// perform your task here
}
});
});
Since I'm not very comfortable with using broadcast I used below approach.
I simply cancel current event (state change) before showing my JQuery Dialog. And if user selects 'yes', then i trigger $state.go, if selects 'cancel/no'- we don't need to do anything, we already cancelled event.
$rootScope.$on('$stateChangeStart',function (event, toState, toParams, fromState, fromParams, options) {
console.log('$stateChangeStart- fromState= '+fromState.name +'-toState= '+toState.name);
/*If user starts to change state, show him a confirmation dialog
* If user selects 'Yes' - continue with state change and go in pause mode for collection.
* If user selects 'No'- stop state change.
*/
if (/*some condition*/) {
/*stateChangeStart gets triggered again by $state.go initiated from our modal dialog-
hence avoid extra cycle of listening to stateChangeStart*/
if (service.stateChangeTriggeredByDialog) {
service.stateChangeTriggeredByDialog = false;
return;
}
if (fromParams.paramName !== toParams.paramName) {
event.preventDefault();
Dialog.confirm({
dialogClass: 'my-customDialog',
template: $translate.instant('STATE_CHANGE_MSG'),
resizable: false,
width: 550
}).then(function () {
console.log('User choose to continue state change');
service.stateChangeTriggeredByDialog = true;
$state.go(toState, toParams, { inherit: false });
}, function () {
console.log('User choose to stop state change');
});
}
}});

Worklight and angular injected $location.path('/') method is not routing in android back button function

I've been digging through the $location.path postings for the past few hours, with no success. I think this example is really close, but I can't find what I'm missing.
$location.path() calls are returning my current path the first time they are clicked, then undefined. When I try to route with $location.path('/') nothing happens.
I have a soft button, bound to $rootScope.backBehavior that works correctly in iOS, but the below binding for Android doesn't work. I've tried doing an $injector.get('$location'); as well but I get the same behavior.
$rootScope.backBehavior = function() {
console.debug("Back button pressed for path: " + $location.path());
//For certain pages we navigate to home page on back button
var homeRoutes = ['contactus', 'aboutus'];
$.each(homeRoutes, function( index, value ) {
if($location.path() == ('/'+value)) {
console.debug("change path to: /");
$location.path('/');
return;
}
});
var backDisabled = ['mustDoSomething'];
$.each(backDisabled, function( index, value ) {
if($location.path() == ('/'+value)) {
console.debug("Back button disabled for route: " + value);
return;
}
});
if($location.path() == '/complexRoute'){
console.debug("Change route to: /anotherPath");
$location.path('/anotherPath');
return;
}
};
// Android back button support
WL.App.overrideBackButton(function(){
$rootScope.backBehavior();
});
It looks like this is the same issue here:
Angular $location.path not working
Try to run the expression as function in the $apply() method:
$rootScope.$apply(function() {
$location.path("/");
});
Try to use $location.replace() after changing the $location.path .

AngularJS UI router $onLocationChangeStart event.PreventDefault does not work

I need a way to interrupt a user navigating to a new page when there are unsaved changes on the current page. I implemented a modified version of the solution here:
http://weblogs.asp.net/dwahlin/cancelling-route-navigation-in-angularjs-controllers
However, what I see in the browser is that as soon as the user clicks on a link, the view changes and the new controller loads completely while the modal dialog is displayed. When the user clicks 'cancel' and event.preventDefault is fired, the user simply ends up on the new view. This is strange because everything I've read indicates that this is the accepted method, and nobody seems to have this issue. Yet I can't for the life of me see what is wrong with my code.
Here's the function in the main app for handling location changes (ModalService just wraps the angular bootstrap $modal service):
onRouteChangeOff = $rootScope.$on('$locationChangeStart', routeChange);
function routeChange(event, newUrl, oldUrl) {
//Navigate to newUrl if the form isn't dirty
if (!$rootScope.unsavedChanges) return;
var modalOptions = {
closeButtonText: 'Cancel',
actionButtonText: 'Ignore Changes',
headerText: 'Unsaved Changes',
bodyText: 'You have unsaved changes. Leave the page?'
};
ModalService.showModal({}, modalOptions).result.then(function () {
$rootScope.unsavedChanges = false;
$location.path(newUrl); //Go to page they're interested in
}
, function () {
event.preventDefault();
});
return;
}
Any ideas?
In case anyone else has this problem, the fix turned out to be quite simple. I moved the code to the stateChangeStart event. Code looks like this:
onRouteChangeOff = $rootScope.$on('$stateChangeStart', routeChange);
function routeChange(event, newState) {
//Navigate to newUrl if the form isn't dirty
if (!$rootScope.unsavedChanges) return;
event.preventDefault();
var modalOptions = {
closeButtonText: 'Cancel',
actionButtonText: 'Ignore Changes',
headerText: 'Unsaved Changes',
bodyText: 'You have unsaved changes. Leave the page?'
};
ModalService.showModal({}, modalOptions).result.then(function () {
$rootScope.unsavedChanges = false;
$state.go(newState); //Go to page they're interested in
});
}

How to cancel routeChange or browser reload when form is incomplete?

There are scenarios like:
Browser reload,
Closing tab
closing browser
Route change (e.g. clicking on links)
Browsers back button was clicked. or history.go(-1). 3 fingers swipe on Macbooks.
that we would want to prevent if the user has filled some sort of form or is in middle of the writing.
I have written this code which works fine but its absolutely not useful if I cant implement it on several textfields. Currently it only check if we are at #/write url. It doesnt check any inputs.
Whats the angular way to deal with this? Whats the best way to check the target textfield. Is a directive the solution?
something like:
<input type="text" warningOnLeave ng-model="title"/>
or
<form warningOnLeave name="myForm">...</form>
$rootScope.$on('$locationChangeStart', function(event, current, previous){
console.log(current);
console.log(previous);
// Prevent route change behaviour
if(previous == 'http://localhost/#/write' && current != previous){
var answer = confirm ("You have not saved your text yet. Are you sure you want to leave?");
if (!answer)
event.preventDefault();
}
});
/**
Prevent browser behaviour
*/
window.onbeforeunload = function (e) {
if(document.URL == 'http://localhost/#/write'){
e = e || window.event;
// For IE and Firefox prior to version 4
if (e) {
e.returnValue = 'You have not saved your text yet.';
}
// For Safari
return 'You have not saved your text yet.';
}
else
return;
}
Forms in Angular have the $dirty/$pristine properties that mark if the user has/hasn't interacted with the form controlls, and the accompanying method $setPristine(). I would base the desired functionality on this feature. Consider:
<form name="theForm" ng-controller="TheCtrl" ...>
This puts the form in the scope of the controller, under the given name. Then something like:
controller("TheCtrl", function($scope, $rootScope) {
$rootScope.$on('$locationChangeStart', function(event, current, previous) {
if( $scope.theForm.$dirty ) {
// here goes the warning logic
}
});
});
Do not forget to call $scope.theForm.$setPristine() where appropriate (i.e. after submitted or cleared).
For the window unload case, you will have to watch the $dirty flag. So in the previous controller:
$scope.$watch("theForm.$dirty", function(newval) {
window.myGlobalDirtyFlag = newval;
});
You have to do this because the window.onbeforeunload event does not have access to the scope of the form. Then, in the global section of your app:
window.onbeforeunload = function (e) {
if( window.myGlobalDirtyFlag === true ) {
// warning logic here
}
};
Again, you may want to clear the global dirty flag when the scope is destroyed, so in the controller:
$scope.$on("$destroy", function() {
window.myGlobalDirtyFlag = false;
});

Resources