amcdnl
amcdnl

Reputation: 8638

AngularJS confirm before route change

Is it possible to listen for route changes AND onwindowunload both to confirm page leave without saving changes?

Use cases:

If the user clicks 'cancel' stop the page / route change.

I've seen a few different examples but none worked quite right.

Upvotes: 16

Views: 31189

Answers (6)

Muhammad Reda
Muhammad Reda

Reputation: 27023

For Angular ES6 Users

export default class myRouteController {

  constructor($scope, $window) {
    this.$scope = $scope;
    this.$window = $window;

    this.$scope.$on('$stateChangeStart', (evt) => {
      if(this.form.$dirty && !$window.confirm('Are you sure you want to leave this page?')) {
        evt.preventDefault();
      }
    });
  }
}

Upvotes: 0

Lawrence Jones
Lawrence Jones

Reputation: 955

You'll find the $locationChangeStart is an event you can listen for to detect for a route change.

Try something like

$rootScope.$on('$locationChangeStart', function (event, next, current) {
  /* do some verification */
});

$route.reload() will prevent the route change from going through.

The distinction between user clicking back or changing the url etc will not make a difference. As browsers don't reload any resources when you alter the url past the # it's all still contained in your angular logic. This means all these methods should trigger the $locationChangeStart event.

@Jon pointed out that the $route service will definitely be of use to you. the .reload() method will prevent the route change from completing but there is much more you can do with the $route service if you look into it - see documentation.

Upvotes: 12

Saurabh
Saurabh

Reputation: 7833

This is also nice link http://weblogs.asp.net/dwahlin/cancelling-route-navigation-in-angularjs-controllers

function init() {

    //initialize data here..
    //Make sure they're warned if they made a change but didn't save it
    //Call to $on returns a "deregistration" function that can be called to
    //remove the listener (see routeChange() for an example of using it)
    onRouteChangeOff = $rootScope.$on('$locationChangeStart', routeChange);
}

function routeChange(event, newUrl) {
    //Navigate to newUrl if the form isn't dirty
    if (!$scope.editForm.$dirty) return;

    var modalOptions = {
        closeButtonText: 'Cancel',
        actionButtonText: 'Ignore Changes',
        headerText: 'Unsaved Changes',
        bodyText: 'You have unsaved changes. Leave the page?'
    };

    modalService.showModal({}, modalOptions).then(function (result) {
        if (result === 'ok') {
            onRouteChangeOff(); //Stop listening for location changes
            $location.path(newUrl); //Go to page they're interested in
        }
    });

    //prevent navigation by default since we'll handle it
    //once the user selects a dialog option
    event.preventDefault();
    return;
}

Upvotes: 4

fracz
fracz

Reputation: 21249

I have created the following service that incorporates information given in the other answers (CoffeeScript, see Plunker ):

angular.module('myModule').service 'ChangeLossPreventer', ($rootScope, $q, $state) ->

  confirmationMessage = null

  service =
    prevent: (message = 'Changed data will be lost. Continue?') ->
      confirmationMessage = message

    prompt: ->
      confirmation = if confirmationMessage then confirm(confirmationMessage) else yes
      if not confirmation
        $q.reject()
      else
        $q.when(confirmation).then ->
          service.cancel()

    cancel: ->
      confirmationMessage = null

  $rootScope.$on '$stateChangeStart', (event, newState, newStateParams) ->
    if confirmationMessage
      event.preventDefault()
      service.prompt().then ->
        $state.go(newState, newStateParams)

  window.onbeforeunload = ->
    confirmationMessage

  service

It is based on angular-ui-router (usage of the $state service). When your website enters the state in which data may be lost, you need to call

ChangeLossPreventer.prevent("Optional warning message.");

When it exists this state (i.e. user saves the data), you need to call:

ChangeLossPreventer.cancel()

By default, warning will appear on state change or page close (i.e. refresh or going to another website). You may also need to verify if it is safe to execute another actions (i.e. do not logout with XHR if there are unsaved changes):

$scope.logout = function() {
    // confirmation will pop up only if there are unsaved changes
    ChangeLossPreventer.prompt().then(function(){
       // logout after confirmation
    });
};

I have created a Plunker that simulates it.

  1. Click the text on home page (page will enter "unsafe" state).
  2. Try clicking other links or refreshing the page - warning should appear.
  3. After "saving" the field, page leaves unsafe state and everything works as before.

Upvotes: 0

Jon Jaques
Jon Jaques

Reputation: 4400

I would just maintain a property on your $rootScope like hasUnsavedEdits.

function confirmLeavePage(e) {
  var confirmed;
  if ($rootScope.hasUnsavedEdits) {
    confirmed = $window.confirm("You have unsaved edits. Do you wish to leave?");
    if (e && !confirmed) { 
      e.preventDefault();
    }
  }
}

$window.addEventListener('beforeunload', confirmLeavePage);

$rootScope.$on("$locationChangeStart", confirmLeavePage);

You might have to tweak the code a bit to handle both conditions. See demo for details.

Upvotes: 4

Daiwei
Daiwei

Reputation: 43526

Demo: http://plnkr.co/edit/Aq8uYg

In the demo, if you change the value of input, you will be noticed when trying to go back.

Listen to $locationChangeStart and use event.preventDefault() to cancel the location change if changes not confirmed.

This method has one advantage over $route.reload(): current controller and models will not be re-instantiated. thus all of your variables are kept the same as user click the "Back" button.

Upvotes: 24

Related Questions