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

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

Related

stop user to go login page when user logged in using ionic

I want that if a user is logged in he can't go to the login and OTP page by hardware back button.
.controller('viewoemCtrl', function($rootScope,$scope,$http,$ionicPopup,$state,$ionicHistory,$ionicLoading,productService,$ionicPlatform) {
$scope.user = {}; //declares the object user
$ionicPlatform.onHardwareBackButton(function () {
if (true) { // your check here
$ionicPopup.confirm({
title: 'Exit from App!',
template: 'are you sure you want to exit?'
}).then(function (res) {
if (res) {
navigator.app.exitApp();
}
})
}
})
})
You don't need to check the click on the back button, because that itself will call a state transition from ui-router.
just register a callback to the $stateChangeStart event fired by ui-router and control there if the user is logged in and if the target state is login, in this case you prevent the transition.
This will handle any case of state transition (the back button, a direct link from the menu etc.)
$rootScope.$on('$stateChangeStart', function(event, toState, toParams,
fromState, fromParams){
//userIsLogged is a flag you should retrieve from your code
if(userIsLogged && toState === 'login'){
event.preventDefault();
}
})
take a look at the documentation

How to prevent the execution of controller upon calling state.go in an if condition?

I am trying to check for a condition in controller and redirect to a different state. But the code after calling State.go is executing as well. The problem I am dealing with is that when I refresh the page using F5, the event beforeunload fires and prompt for user to choose between 'Leave this page' and 'Stay on this page'. If I choose 'Stay on this page', it leaves the page as is and it looks good. But if I choose 'Leave this page', it tries to reload the page so I am trying to redirect to a different state upon missing some data.
var windowElement = angular.element($window);
windowElement.on('beforeunload', function (event) {
console.log('refreshing the page');
var path = $location.path();
if (path == '/record' || path == '/recordcreated') {
event.preventDefault();
}
});
$scope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams, options) {
//Do not allow to go from record created page to record page.
if (fromState.name == 'recordcreated' && toState.name == 'record') {
event.preventDefault();
$state.go('home');
}
});
$scope.selectedRecord = service.GetSelectedRecord();
//Check if null and redirect to home state
if ($scope.selectedRecord == null)
{
$state.go('home', {}, { reload: true });
}
//Continuing to execute this line
$scope.Balance = service.GetBalance();
How can I make it stop executing the above line after calling state.go ? Thanks for any suggestions.
Two ways to achieve this:
Add the keyword return to the line you wish the code to stop executing at:
return $state.go('home', {}, { reload: true });
Or, append your if statement with an else block:
if ($scope.selectedRecord == null) {
$state.go('home', {}, { reload: true });
} else {
//Continuing to execute this line
$scope.Balance = service.GetBalance();
}

Clear $stateParams for specific view

In my Ionic app I have defined a parameter as null for default in my home state. So when this parameter is defined as true, some actions are performed, in this case a modal appears.
Problem is, when I switch to another state, and go back, this param keeps it's value as true.
How can I clear a specific param for a specific view?
This is the piece of code:
if ($stateParams.watchTutorial === true) {
$rootScope.$broadcast('startTutorial');
$stateParams.watchTutorial = null;
}
Tried to set it to null but didn't work.
To resume, navigating:
Home -> View 1 -> Tap button -> Home (Param: {watchTutorial: true}). Great, goes home and modal appears. Keep navigating in home..
Home -> View 2 -> Go back Home (No params specificed), watchTutorial = true anyways and modal screen appears. And it shouldn't.
Any ideas? Thank you.
I would listen for the $stateChangeSuccess event and then react on the available information.
$rootScope.$on('$stateChangeSuccess',
function(event, toState, toParams, fromState, fromParams){
if(fromState == "[the_state_of_view_2]"){
toParams.watchTutorial = false;
//or toParams = {}
}else{
if ($stateParams.watchTutorial === true) {
$rootScope.$broadcast('startTutorial');
$stateParams.watchTutorial = null;
}
})
Ok, I solved it thanks to your answer #Andre Kreinbring.
This made the job:
$rootScope.$on('$stateChangeSuccess',
function(event, toState, toParams, fromState, fromParams) {
if(toState.name == 'app.home') {
if(toParams.watchTutorial === true) {
$rootScope.$broadcast('startTutorial');
toParams.watchTutorial = false;
}
}
});

How to prevent default browser back button functionality in 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;
}
}
});

AngularJS- Prompting the user before routing to other controller to save changes

I have a form in a controller.
If there are unsaved change I want to warn the user about loosing them when leaving.
First I tried:
$scope.$on('$locationChangeStart', function (event, next, current) {
if ($scope.settingsForm.$dirty) {
event.preventDefault();
$scope.theUserWantsToLeave(function (result) {
if (result === "LEAVE") {
$location.path($location.url(next).hash());
$scope.$apply();
}
});
}
The code above throws an error in the line $scope.$apply();:
Error: $digest already in progress
removing this line just don't execute the redirect.
What would be the right way to do it?
===
Edit:
Other option I tried is handling it by reacting only when I need to cancel the redirection:
$scope.$on('$locationChangeStart', function (event, next, current) {
if ($scope.settingsForm.$dirty) {
$scope.theUserWantsToLeave(function (result) {
if (result === "STAY_HERE") {
event.preventDefault();
}
});
}
});
when doing things this way, the UI is breaking (I see the dialog and then it gone).
Seems like I can't call another async method while handling event.
I've managed to interrupt by the route change by listening for $locationChangeSuccess and then assigning $route.current to the last route.
Also, if $scope.theUserWantsToLeave is async, then the callback passed to it may fire too late to stop the route change. You could get around an async call by using a blocking flag, such as okToDiscardChanges in my examples.
JS:
$scope.okToDiscardChanges = false;
var lastRoute = $route.current;
$scope.$on('$locationChangeSuccess', function () {
if ($scope.settingsForm.$dirty && !$scope.okToDiscardChanges) {
$route.current = lastRoute;
$scope.errorMessage = 'You have unsaved changes. Are you sure you want to leave?';
}
});
HTML:
<form name="settingsForm">
<!-- form stuff -->
</form>
<div ng-show="errorMessage">
{{ errorMessage }}
<label>
<input type="checkbox" ng-model="okToDiscardChanges"> Discard Changes
</label>
</div>
Hope this works!

Resources