14

I'm using a module from the UI Boostrap extensions (http://angular-ui.github.io/bootstrap). The module actually serves as a loading dialog and is automatically closed when a set of web service data is returned to my Angular code. As the data on this page is loaded automatically the dialog comes up immediately.

All this works great when I hit the page in question for the the first time or simply refresh it. The problem occurs when I go to a deeper page and then try and navigate back to the original page (with the dialog) via the browser's back button. The dialog never goes away despite all the fact that all the data is returned and the module's dismiss() call has been made.

I've traced this down to the promise to open the dialog appears to be happening after the dismiss call but, again, only when the page is loaded via the back button. The dismiss call never closes anything because it hasn't been added yet (I've confirmed this in the debugger).

The question I have is how could I handle this? Is there a solid way to catch the completion of the page loading via Angular and double check that the dialog closed? Is there a better way via UI Bootstrap's api?

I know this is rather unusual case but any thoughts on it would be great.

Thanks!

1

4 Answers 4

10

@HankScorpio's solution is good, but I think there may be a simplified option now.

There is no need to store the current modal anymore, if you register either a $locationChangeStart or $routeChangeStart listener with $uibModalStack injected and call $uibModalStack.dismissAll(). $locationChangeStart has the benefit of working for both ngRoute and uiRoute.

i.e. If only for the one page, then in your controller you'd have:

angular.module('app')
    .controller('ctrl', ['$scope', '$uibModalStack', ctrl]);

function ctrl($scope, $uibModalStack) {
    $scope.$on('$locationChangeStart', handleLocationChange);

    function handleLocationChange() {
        $uibModalStack.dismissAll();
    }
}

If you want to do this for all pages then define this in a factory that is always loaded or just an app.run code segment:

angular.module('app')
    .run(['$rootScope', '$uibModalStack', setupUibModal]);

setupUibModal($rootScope, $uibModalStack) {
    $rootScope.$on('$locationChangeStart', handleLocationChange);

    function handleLocationChange() {
        $uibModalStack.dismissAll();
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

Just a note that depending on which version of ui-bootstrap you may use $modalStack instead of $uibModalStack (current is $uibModalStack)
If you want the back button to close the dialog but not actually go back in the history, you can check if $uibModalStack.getTop() returns anything, and only in that case dismiss and also event.preventDefault() (event is passed to handleLocationChange()).
5

Here is the simple solution when using ui-router for state change

Closing modal popup on the back button click in angularjs

App.run(['$rootScope', '$modalStack', function ($rootScope, $modalStack) {
   $rootScope.$on('$stateChangeStart', function (event) {
        var top = $modalStack.getTop();
        if (top) {
            $modalStack.dismiss(top.key);
        }
    })
}]);

hope this will save lot of time for people who are breaking heads

Comments

4

I've run into this same problem. Here's how I fixed it.

1) Create a service to abstract the opening and closing of a modal and track which one is open (necessary for step 2). Instead of calling $modal.open() directly, call ModalService.open(). Here you go, you can have the one I wrote:

(function () {
    'use strict';

    var theModule = angular.module('services.modalService', ['ui.bootstrap']);

    theModule.factory('ModalService', function ($modal) {
        var service = {};
        var currentModal;
        var clearModal = function () {
            currentModal = undefined;
        };

        service.getCurrentModal = function () {
            return currentModal;
        };

        service.open = function (options) {
            currentModal = $modal.open(options);
            currentModal.result['finally'](clearModal);
            return currentModal;
        };

        return service;
    });
}());

2) In a controller, add an event listener to $routeChangeStart, this event will fire whenever someone hits the back button.

$scope.$on('$routeChangeStart', function(){
  var currentModal = ModalService.getCurrentModal();
  if(angular.isDefined(currentModal)){
    currentModal.dismiss('cancel');
  }
});

3) Your modals should now close when a user hits back.

4) Enjoy.

5 Comments

Would this work on the page being nav'd from or for the destination page? The problem is not the page that I'm going from being cleaned up but rather the difference sequence of events for loading on the destination page.
I actually ended up doing something similar to you but I hooked into a ng-show handler on an empty span at the bottom of the page (a total hack). I'm hoping there is a better way but it does 'work'.
Re-reading your original post, it sounds like the cause of this problem is that the modal popup is being opened 100% of the time (when that page is loaded) even if it's not needed, is that correct?
Not quite: the dialog is popping up 100% of the time when the page loads but that's intended. The page has to rebuild state and required data from the server, hence the dialog. The problem is that with the back button the destination page never closes the dialog, even after all the state rebuilding has completed. Again this is because UI bootstrap reverses the order in which the open and close promises are run for the dialog. This occurs when the page is loaded as a result of a back button nav.
I'm not sure I follow. So your code is attempting to close the modal before it's open? Can you provide some sample code?
0

IMPROVEMENT:

I found the answer from HankScorpio to be the best out there. I wanted to include this snippet for those using ui-router and their recommendation for stateful modals.

1) I wanted the result.finally(...) to jump to a parent state;
2) I wanted to control the closing of the modal from $stateProvider config, NOT through rigging a controller and adding a listener to $routeChangeStart

Here is an example of a state that opens (and closes) it's modal:

        .state('product.detail', {
            url: '/detail/{productId}',
            onEnter: /*open-modal logic*/,
            onExit: ['ModalService', function (ModalService) { ModalService.close()} ]                
        })

I made ModalService aware of $state so that the result of closing a modal could jump to a parent view:

a. Add an isStateful flag to modalService.open(...):

service.open = function (options, isStateful) {
    currentModal = $uibModal.open(options);
    currentModal.result.finally(function () {
        clearModal(isStateful);
    });
    return currentModal;
};

so that clearModal will return to previous state:

  var clearModal = function (isStateful) {
        currentModal = undefined;
        if (isStateful)
            $state.go('^');
    };

Finally, add the closeModal() function called above (not a "stateful" close, simply a dismissal):

  service.close = function() {
        if (currentModal) {
            currentModal.dismiss().then(function () {
                clearModal();
            })
        }
    }

The benefits of this are that back button functionality is controlled at state config level, not through a listener.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.