In order to prevent view changes on a form with edits, I am using $locationChangeStart to intercept the view change. In that function, I am attempting to use a bootstrap dialog to prompt the user.
It all works ok, until the part where I call $location.path(current) to change the view. Instead of navigation to the appropriate route, it goes to the default route (the home page). Why is this?
Here is the code I am using in my controller:
function onNavigate() {
var turnOff = $scope.$on('$locationChangeStart',
function (event, current, previous) {
if (vm.canSave) {
dialogService.confirmationDialog('Are you sure?', 'You have unsaved changes, are you sure you want to abandon your form?')
.then(function() {
turnOff();
$location.path(current);
});
event.preventDefault();
});
}
In the debugger, the value of current is something like
http://localhost:3091/#/participant/24
at the $location.path line, however my application ends up at
http://localhost:3091/#/
Related
I open a UI-Bootstrap Modal using the $modal.open({...}) method. I need this to close when the user presses the back button.
The result promise returned by the open() method is not useful in this case as it cannot detect the state change due to the back button. Right now when the back button is pressed, the state changes but the modal stays open.
Basically I am having the exact problem as this question but even though it has a selected answer, the problem is not solved as evidenced from the comments. This other question is similar but also doesn't solve the back button issue.
I need some way to detect that the current state has changed from within the controller and call $modalInstance.close() or the $scope.$close() methods.
I could listen for $stateChangeStart event and check the fromState argument to conditionally close the modal. But then this event would unnecessarily keep firing for all subsequent state changes too.
UPDATE: So I tried listening for the event and deregistered it as soon as it is fired for the first time. This way I get to listen for the back button state change and then stop it when I want. The final code for the modal state is as follows:
$stateProvider.state('itemList.itemNew', {
url: '/new',
onEnter: function($state, $modal) {
$modal.open({
templateUrl: "/static/partials/item/form.html",
controller: function($http, $scope, $modalInstance) {
$scope.editableItem = {};
$scope.saveItem = function(item) {
$http.post('/api/item', item)
.success(function(data) {
$modalInstance.close(data);
alert("Saved Successfully");
}).error(function(data) {
alert("There was an error.");
});
};
//Register listener specifically for the back button :(
deRegister = $scope.$on('$stateChangeSuccess',
function(event, toState, toParams, fromState, fromParams) {
if (toState.name === 'itemList' &&
fromState.name === 'itemList.itemNew') {
$modalInstance.close();//Close the modal
deRegister();//deRegister listener on first call
}
}
);
}
}).result.then(function() {
//Promise Resolved, Modal Closed.. So reload
$state.go("^", null, {
"reload": true
});
}, function() {
//Promise Rejected, Modal Dismissed.. no reload
$state.go("^");
});
},
});
I still think there should be a better way to do it. Constellates apparently decided to dump modal.js from ui-bootstrap altogether. Should I do the same and simply render the modal using plain Bootstrap CSS out of a <ui-view/>?
I needed to address the same issue. Maybe a slightly different setup, but I believe it may work for you.
I am using the angular-modal-service, which I believe is running on bootstrap anyway.
Inside the controller for the modal I used the following:
$scope.$on('$stateChangeStart', function() {
$scope.stateChange();
});
$scope.stateChange = function() {
// Manually hide the modal using bootstrap.
$element.modal('hide');
// Now close as normal, but give 500ms for bootstrap to animate
close('', 500);
};
The second function is just the manual way of exiting the modal as per the docs under "I don't want to use the 'data-dismiss' attribute on a button, how can I close a modal manually?". It says:
All you need to do is grab the modal element in your controller, then call the bootstrap modal function to manually close the modal. Then call the close function as normal
So now, if a state change happens (including a user-initiated back-button click), the modal gracefully zips away.
Here is my problem, I have two views (View1 and View2) and a controller for each view (Ctrl1 and Ctrl2). In View1 I'm trying to warn the user before he leaves the page accidentally without saving changes.
I'm using window.onbeforeunload, which works just fine, but my problem is that even though I have onbeforeunload only on one controller, the event seems to be triggered in the second controller as well! But I don't have even have that event in there. When the user leaves a page and there are unsaved changes, he gets warned by the onbeforeunload, if the user dismiss the alert and leaves the page, is he taken back to the second view, if the user tries to leave now this other view, we would also get warned about unsaved changes! When that's not even the case! Why is this happening?
I'm also using $locationChangeStart, which works just fine, but only when the user changes the route, not when they refresh the browser, hit 'back', or try to close it, that's why I'm forced to use onbeforeunload (If you a better approach, please let me know). Any help or a better approach would be greatly appreciated!
//In this controller I check for window.onbeforeunload
app.controller("Ctrl1", function ($scope, ...)){
window.onbeforeunload = function (event) {
//Check if there was any change, if no changes, then simply let the user leave
if(!$scope.form.$dirty){
return;
}
var message = 'If you leave this page you are going to lose all unsaved changes, are you sure you want to leave?';
if (typeof event == 'undefined') {
event = window.event;
}
if (event) {
event.returnValue = message;
}
return message;
}
//This works only when user changes routes, not when user refreshes the browsers, goes to previous page or try to close the browser
$scope.$on('$locationChangeStart', function( event ) {
if (!$scope.form.$dirty) return;
var answer = confirm('If you leave this page you are going to lose all unsaved changes, are you sure you want to leave?')
if (!answer) {
event.preventDefault();
}
});
}
//This other controller has no window.onbeforeunload, yet the event is triggered here too!
app.controller("Ctrl2", function ($scope, ...)){
//Other cool stuff ...
}
I tried checking the current path with $location.path() to only warn the user when he is in the view I want the onbeforeunload to be triggered, but this seems kinda 'hacky' the solution would be to not execute onbeforeunload on the other controller at all.
I added $location.path() != '/view1' in the first which seems to work, but it doesn't seem right to me, besides I don't only have two views, I have several views and this would get tricky with more controllers involved:
app.controller("Ctrl1", function ($scope, ...)){
window.onbeforeunload = function (event) {
//Check if there was any change, if no changes, then simply let the user leave
if($location.path() != '/view1' || !$scope.form.$dirty){
return;
}
var message = 'If you leave this page you are going to lose all unsaved changes, are you sure you want to leave?';
if (typeof event == 'undefined') {
event = window.event;
}
if (event) {
event.returnValue = message;
}
return message;
}
//This works only when user changes routes, not when user refreshes the browsers, goes to previous page or try to close the browser
$scope.$on('$locationChangeStart', function( event ) {
if (!$scope.form.$dirty) return;
var answer = confirm('If you leave this page you are going to lose all unsaved changes, are you sure you want to leave?')
if (!answer) {
event.preventDefault();
}
});
}
Unregister the onbeforeunload event when the controller which defined it goes out of scope:
$scope.$on('$destroy', function() {
delete window.onbeforeunload;
});
I just attempted the above solution, and that wasn't working for me. Even manually typing delete window.onbeforeunload in the console wouldn't remove the function. I needed to set the property to undefined instead.
$scope.$on('$destroy', function(e){
$window.onbeforeunload = undefined;
});
For those of you using angular-ui-router you would use is $stateChangeStart instead of $locationChangeStart, e.g.
$scope.$on('$stateChangeStart', function (event){
if (forbit){
event.preventDefault()
}
else{
return
}
})
As an update to this, for anyone used angular-ui-router I'd now pass $transitions into your controller and use;
$transitions.onStart({}, function ($transition)
{
$transition.abort();
//return false;
});
I would like to warn user and get a confirmation before the user navigates away from certain pages in my application, like composing a new message view. Which events can i capture and cancel/continue to make this happen ?
You should handle the $locationChangeStart event in order to hook up to view transition event, so use this code to handle the transition validation in your controller/s:
$scope.$on('$locationChangeStart', function( event ) {
var answer = confirm("Are you sure you want to leave this page?")
if (!answer) {
event.preventDefault();
}
}
also you can use this directive angularjs-unsaved-changes which would be much more reusable than writing it per controller..
If you have say a link or button navigating to another route or state, you could simply show a modal confirming the navigation:
Go Away...
$scope.goToAnotherState = function(){
//show modal and get confirmating however your like
if(modalResponse.Ok){
$state.go('anotherState');
}
}
another option would be to do this in the $locationChangeStart event of ui-router, but if you're looking to this only here n there, then the first approach is better. $locationChangeStart approach:
$rootScope.$on('$locationChangeStart', function (event, next, prev) {
event.preventDefault();
//show modal
if(modalResponse.ok){
var destination = next.substr(next.indexOf('#') + 1, next.length).trim();
$location.path(destination)
}
}
When defining a state, use the onExit callback, which is a transition hook:
An onEnter/onExit declared on a state is processed as a standard Transition Hook. The return value of a hook may alter a transition.
.state('foo', {
//dont return a value else it the transition will wait on the result to resolve (Promise)
onEnter: (MyService) => { MyService.doThing(); },
//return false to cancel the transition; i.e. prevent user from leaving
onExit: (MyService) => { return MyService.isSatisfied(); }
});
https://ui-router.github.io/ng1/docs/latest/interfaces/ng1.ng1statedeclaration.html#onexit
I need to open the menu automatically when navigate to a specific page.
but the event is ignored.
I created the menu controller:
.controller('MenuController', function ($scope, $ionicSideMenuDelegate) {
$scope.toggleLeft = function() {
$ionicSideMenuDelegate.toggleLeft();
}; })
and the specific page controller:
.controller('Sem_ConsultasCtrl', function ($scope) {
$scope.toggleLeft();
$scope.btn = function () { $scope.toggleLeft(); }
})
in my specific page i have a directive ng-click="btn()" wich works (toggles side-menu when click on button).
but if I call ' $scope.toggleLeft(); ' outside of btn() to automatically open the side menu when navigate to specific page nothing happens.
I found the problem:
when I call '$scope.toggleLeft();' outside of btn() the page/template still has not loaded/rendered the DOM. and when I click on button (btn()) works because DOM is already rendered.
to automatically open the side-menu I need to only call '$scope.toggleLeft();' when DOM is already and for achieve that I need to define a Watcher wich do something when occurs some modification to my template:
$timeout(function () {
$scope.toggleLeft();
});
$timeout(function () { //runs after DOM is render} );
This way, is working :)
EDIT:
I was going through my answers and I noticed that this answer was not correct.
calling $timeout triggers a digest cycle that captures differences in the DOM and updates it.
other events like clicking a button or writing in a input text triggers a digest cycle, thats why the changes only happened when clicked the button
I have a situation where I want the URL in the address bar to change, but I don't want Angular to refresh the controller or view like in a normal route change.
So, going to /data then clicking on a link saying /data/just-blue would only refresh a part of the ngView not the entire view.
How is that possible?
Currently I have those 2 paths defined as separate .when() paths in the app .config() method.
Thanks.
So basically you can do the following:
$scope.$on("$locationChangeStart", function (event, next, current) {
console.log(event, next, current);
console.log(next);
if (next == 'http://localhost:9000/data/just-blue') {
setTimeout(function() {
$window.history.pushState("object or string",
"Title", '/data/just-blue)'},20);
event.preventDefault();
// ... update your view
}
});
Mind that I used setTimeout instead of $timeout.
Here's another solution: http://joelsaupe.com/programming/angularjs-change-path-without-reloading/ (not using pushState)