prevent redirect behaviour in angularjs app-Collection of common programming errors

There are two DOM events at the root of this whole problem you’re trying to solve: onhashchange and onbeforeunload. onhashchange can be checked and prevented, however, the Back button on your browser will not trigger onhashchange. What’s worse, onbeforeunload will not trigger if the page doesn’t reload, which means if you hit Back to go to a previous hash on the page, it will not fire. Because of this, if you press Back to to go a previous route it will still leave your form.

There is also currently an oustanding issue on Angular’s todo list about how they’re going to allow for the cancellation of a route. I’d presume the back button to a hash issue is what’s hold them up at this point.

So in the end you may want to re-engineer your solution to do something a little more drastic if you want to prevent all navigation away from your form once it’s been edited. Something like storing all of the data the form is editing in $rootScope, along with a flag to show that it’s dirty but incomplete, then add an event handler to routeChangeStart that checks those values and sends you back to the form.

Here’s how that would work (and a plunker if you’re interested):

app.config(function($routeProvider) {
  //set up the routes. (Important because we're going to navigate
  // BACK to them.)
  $routeProvider.when('/Form', {
    controller: 'FormCtrl',
    templateUrl: 'form.html'
  }).otherwise({
    controller: 'HomeCtrl',
    template: '

Home

' 
  });
});

app.run(function($rootScope, $location){ 
  //set up your rootScope formData object.
  $rootScope.formData = {};

  //add a routing event to check the route
  // and whether or not the data has been editted and then 
  // send it back to the proper form.
  $rootScope.$on('$routeChangeStart', function() {
    if($location.path() != '/Form' && $rootScope.formData.dirty && 
      !$rootScope.formData.complete && !confirm('Do you want to leave this form?')) {
      $location.path('/Form');
    }
  });

  //handle outright navigating away from the page.
  $(window).on('beforeunload', function() {
     if($rootScope.formData.dirty && 
      !$rootScope.formData.complete) {
         return 'Are you sure you want to navigate away from this form?';
     }
  });
});

app.controller('FormCtrl', function($scope) {
  $scope.$watch(function (){
    return $scope.myForm.$dirty;
  }, function(dirty) {
    $scope.formData.dirty = $scope.formData.dirty | dirty;
  })
});

other thoughts

Initially I had developed a directive to help with this, but I realized that it wouldn't work because of the issues I mentioned above. Regardless, for posterity's sake, here it is:

app.directive('form', function ($window){
  return {
    restrict: 'E',
    link: function(scope, elem, attrs) {

      //check for a prevent-if-dirty attribute on your form tag
      if(attrs.preventIfDirty !== undefined) {

        // first off, stop routing hash changes from 
        // changing the page.
        scope.$on('$locationChangeStart', function(event) {
          if(scope.testForm.$dirty) {
            event.preventDefault();
          }
        });

        // a little setup for our next piece
        var formName = attrs.name;
        function showWarning() {
          return 'You have changed the form';
        }

        // Now stop browser navigation from moving away
        // from your dirty form.
        scope.$watch(function (){
          return scope[formName].$dirty;
        }, function(dirty) {
          if(dirty) {
             $(window).on('beforeunload', showWarning);
          } else {
             $(window).off('beforeunload', showWarning);
          }
        });
      }
    }
  };
});

Here’s a plunker demonstrating it.

Originally posted 2013-12-02 01:31:11.