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!

Shlomi Hassid
  • 6,500
  • 3
  • 27
  • 48
Siegmund Nagel
  • 1,321
  • 2
  • 11
  • 19
  • Possible duplicate of [Is there a way to automatically close Angular UI Bootstrap modal when route changes?](https://stackoverflow.com/questions/23762323/is-there-a-way-to-automatically-close-angular-ui-bootstrap-modal-when-route-chan) – Johann Jan 19 '18 at 01:03

4 Answers4

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();
    }
}
bigstones
  • 15,087
  • 7
  • 65
  • 82
Mark van Proctor
  • 733
  • 7
  • 13
  • 2
    Just a note that depending on which version of ui-bootstrap you may use $modalStack instead of $uibModalStack (current is $uibModalStack) – TheMcMurder Jan 25 '16 at 21:42
  • 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()`). – bigstones Jun 21 '17 at 13:12
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

andrew.fox
  • 7,435
  • 5
  • 52
  • 75
VR1256
  • 1,266
  • 4
  • 28
  • 56
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.

HankScorpio
  • 3,612
  • 15
  • 27
  • 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. – Siegmund Nagel Apr 09 '15 at 01:21
  • 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'. – Siegmund Nagel Apr 09 '15 at 01:23
  • 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? – HankScorpio Apr 09 '15 at 17:24
  • 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. – Siegmund Nagel Apr 09 '15 at 20:55
  • 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? – HankScorpio Apr 09 '15 at 21:07
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.

tdeets
  • 56
  • 4