ngDialog closed event fires multiple times (AngularJS) - angularjs

The issue we are having is when we listen for the event 'ngDialog.closed', we found that it is firing multiple times. We are only opening the dialog once, so we assume that closing the same modal, this event should only fire once as well. There are multiple modals across the application, and we listen for this specific modal by attaching it to $rootScope, like so:
$rootScope.$on('ngDialog.closed', function (e, $dialog) {
console.log("CLOSED TEHE MODAL", $dialog);
console.log("EVENT", e);
if($rootScope.modalFinished && localStorage.getItem("tourComplete") !== "true"){
$rootScope.modalFinished = false;
console.log("THIS IS LOCAL STORE:", localStorage.getItem("tourComplete"));
$scope.CallMe();
}
});
The reason we are listening via $rootScope is to pass this "closed" event from one controller(the modal) to another controller(home screen) that has already been loaded before the modal controller.
We have noticed that on the 'ngDialog.closed' event, multiple events are being fired (usually up to 2 to 3 times per closing of a modal).
Here is where we open the ngDialog (in the login controller):
$state.go('app.dashboard')
if(AclService.can('edit')|| AclService.can('admin')){
$scope.$on('$stateChangeSuccess', function onStateSuccess(event, toState, toParams, fromState){
$rootScope.modalFinished = true;
CauseService.get().then(function(result){
var cause = result[0].attributes;
if(!cause.has_backlinked || !cause.has_verified_address){
// $rootScope.modalFinished = true;
ngDialog.open({
template: '../../app/views/dashboard/progress_modal.html',
showClose: false,
className: 'ngdialog-theme-default progressModal',
controller: 'ProgressModalController',
resolve:{
'Cause': ['CauseService', function(CauseService){
// console.log("CauseService in routes", CauseService.get());
return CauseService.get();
}]
}
});
}
});
});
}
Any help would be greatly appreciated.
Thanks so much.
-M-

Alright so after lots of trial and error, I concluded that due to attaching the 'ngDialog.close' event to $rootScope AND the controller we are listening on has already loaded before the modal's controller has loaded, each controller will log the event of the modal being closed.
To remedy this problem, I attached the 'ngDialog.close' event to $scope like so:
$scope.$on('ngDialog.closed', function (e, $dialog) {
if($rootScope.modalFinished && localStorage.getItem("tourComplete") !== "true"){
$rootScope.modalFinished = false;
console.log("THIS IS LOCAL STORE:", localStorage.getItem("tourComplete"));
console.log("****************NGDIALOG IS CALLED")
$scope.CallMe();
}
});
And now the desired behavior of only one 'ngDialog.close' event is fired within the dashboard controller. This solution also solves the problem of this listener being fired on other modals within other $states or controllers.
Hope this answer can help someone out and would appreciate any feedback!!!!
-M-

//In nebular (angular 4+)
// Closed event identify by below code in nebular lib in Angular 4+
this.dialogService.open(UpdateUseItemComponent, {
hasBackdrop:true ,
closeOnEsc:true,
autoFocus:true,
context: {
data: event
}
}).onClose.subscribe(result => {
this.getStock();
});

Related

Returning data from angular $uibModal backdrop click

I'm trying to pass back a value from a uibModal. I can define the return if the user clicks the modal's close button
$scope.close = function () {
$modalInstance.close($scope.editMade);
};
But this doesn't work if the user click the backdrop area.
How can I define a return value for that particular event?
When you click on the backdrop outside, it does a dismiss internally.
Try using this inside modal:
$modalInstance.dismiss($scope.editMade);
And use this to handle data:
instance.result.then(function(){
//Get triggers when modal is closed
}, function(){
//gets triggers when modal is dismissed. You can basically handle data here
});
Check this working plunker. It uses the dismiss as I mentioned
http://embed.plnkr.co/YdWmqYPFBNbv4vhQZc4t/
Passing custom data to your parent controller when modal is dismissed:
Code in Modal Controller:
$scope.$on("modal.closing",function(){
$rootScope.$broadcast("modalClosing",$scope.editMade);
});
Then in your parent controller:
$scope.$on("modalClosing",function(event,value){
console.log(value); //value should be $scope.editMade
});
You can learn more by reading the documentation here:
https://github.com/angular-ui/bootstrap/tree/master/src/modal/docs
In the modal controller, you can do something like this:
$scope.$on('modal.closing', function(event, reason, closed) {
if (reason == "backdrop click" || reason == "escape key press")
{
event.preventDefault();
$uibModalInstance.close({"data": somedata});
}
});
This way you'll always get the success callback on modalInstance
modalInstance.result.then(function(response) {
// here response will be whatever is passed. in this sample {"data": somedata}
});

Angular UI Bootstrap Module Not Closing on Back Button

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!
#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();
}
}
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
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.
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.

Detect ui-router state change from within controller and close Modal accordingly

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.

What is a correct way to bind document level key events in AngularJS specific only to a certain route/controller?

I have a single page app that opens a gallery. I want to bind document level keyup event (for keyboard gallery controlls) only when the gallery is open, ie. when route matches
.when('/reference/:galleryId/:imageId/', { templateUrl: '/partials/gallery.htm', controller: 'galleryController', controllerAs: 'reference' })
and I want to unbind it when I leave this route.
One thing that might be a problem is, I block reloading the view between images within the same gallery with this:
.run(['$route', '$rootScope', '$location', function ($route, $rootScope, $location) {
var original = $location.path;
$location.path = function (path, reload) {
if (reload === false) {
var lastRoute = $route.current;
var un = $rootScope.$on('$locationChangeSuccess', function () {
$route.current = lastRoute;
un();
});
}
return original.apply($location, [path]);
};
}])
Demo (Click on "Fotografie" to open the gallery)
http://tr.tomasreichmann.cz/
Angular wiz to the rescue?
Thank you for your time and effort
You could bind a keyup event to $document in your controller's constructor and then unbind the event when the controller's $scope is destroyed. For example:
.controller('galleryController', function ($scope, $document) {
var galleryCtrl = this;
function keyupHandler(keyEvent) {
console.log('keyup', keyEvent);
galleryCtrl.keyUp(keyEvent);
$scope.$apply(); // remove this line if not need
}
$document.on('keyup', keyupHandler);
$scope.$on('$destroy', function () {
$document.off('keyup', keyupHandler);
});
...
});
There will be nothing left behind when the view become inactive.
If you feel it isn't right to do this in the controller, you could move this into a custom directive and place it in a template of the view.
Finally I stuck with
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:og="http://ogp.me/ns#"
xmlns:fb="http://www.facebook.com/2008/fbml"
lang="cz"
ng-app="profileApp"
ng-keyup="$broadcast('my:keyup', $event)" >
Not sure if this is good practice, but it registers within my controller
$scope.$on('my:keyup', function(event, keyEvent) {
console.log('keyup', keyEvent);
galleryCtrl.keyUp(keyEvent);
});
And doesn't do anything when the current route is not active
I found this answer here: https://groups.google.com/forum/#!searchin/angular/document$20level$20events/angular/vXqVOKcwA7M/RK29o3oT9GAJ
There is another way to bind it globally which wasn't my goal, the original code in question did what I needed.

Is there a way to automatically close Angular UI Bootstrap modal when route changes?

I've got links in templates inside modals. When I click them, the current page changes, but the overlay and modal stay. I could add ng-click="dimiss()" to every link in all templates in modals, but is there a better way? E.g. to close it automatically on successful route change or add just one ng-click per template to handle all links?
If you want all the opened modals to be closed whenever a route is changed successfully, you could do it in one central place by listening to the $routeChangeSuccess event, for example in a run block of your app:
var myApp = angular.module('app', []).run(function($rootScope, $uibModalStack) {
$uibModalStack.dismissAll();
});
Here you can see that the $uibModalStack service gets injected on which you can call the dismissAll method - this call will close all the currently opened modals.
So, yes, you can handle modals closing centrally, in one place, with one line of code :-)
A better way is to see that whenever a Popup (Modal) is open, on Browser Back button click (or Keyboard Back), we stop the URL change and just close the Popup. This works for a better User Experience in my Project.
The Browser Back button works normally if there is no Modal opened.
use:
$uibModalStack.dismiss(openedModal.key);
or
$uibModalStack.dismissAll;
Sample code:
.run(['$rootScope', '$uibModalStack',
function ($rootScope, $uibModalStack) {
// close the opened modal on location change.
$rootScope.$on('$locationChangeStart', function ($event) {
var openedModal = $uibModalStack.getTop();
if (openedModal) {
if (!!$event.preventDefault) {
$event.preventDefault();
}
if (!!$event.stopPropagation) {
$event.stopPropagation();
}
$uibModalStack.dismiss(openedModal.key);
}
});
}]);
I don't actually use Angular UI Bootstrap, but from looking at the docs, it looks like there is a close() method on the $modalInstance object.
So taking the example from the docs, this should work:
var ModalInstanceCtrl = function ($scope, $modalInstance, items) {
$scope.items = items;
$scope.selected = {
item: $scope.items[0]
};
$scope.ok = function () {
$modalInstance.close($scope.selected.item);
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
// this will listen for route changes and call the callback
$scope.$on('$routeChangeStart', function(){
$modalInstance.close();
});
};
Hope that helps.
I resolved this issue by doing something like this:
$rootScope.$on('$stateChangeSuccess',
function(event, toState, toParams, fromState, fromParams){
$modalStack.dismissAll();
});
I am keeping this logic in the modal controller. You can listen to $locationChangeStart event and close modal there. It is also good to remove listener after, especially if you have registered a listener on $rootScope:
angular.module('MainApp').controller('ModalCtrl',['$scope','$uibModalInstance',
function ($scope, $uibModalInstance) {
var dismissModalListener = $scope.$on('$locationChangeStart', function () {
$uibModalInstance.close();
});
$scope.$on('$destroy', function() {
dismissModalListener();
});
}]);
check for the respective route condition in the event $stateChangeSuccess and
then close the open bootstrap modals globally using the class like this:
$rootScope.$on('$stateChangeSuccess',
function(event, toState, toParams, fromState, fromParams){
//hide any open bootstrap modals
angular.element('.inmodal').hide();
});
If you want to hide any other modals such as angular material dialog ($mdDialog) & sweet alert dialog's use angular.element('.modal-dialog').hide(); & angular.element('.sweet-alert').hide();
Adding this an alternative answer.
Depending on your project, using $uibModalStack.dismissAll() could trigger an error message.
As explained by JB Nizet in this answer, it is caused by dismissAll() rejecting a promise, leading to a 'failure' callback as opposed to a 'success' callback triggered by close().
Said promise rejection could trigger a possibly unwanted error handling procedure.
Given there is no closeAll() in $uibModalStack, I used this:
var modal = $uibModalStack.getTop();
while (modal && this.close(modal.key)) {
modal = this.getTop();
}
This is the same behaivour as $uibModalStack.dismissAll() but utilizes.close() instead of .dismiss().
I couldn't find any documentation describing public methods for $uibModalStack, thus, in case anybody is interested in using/looking at other methods available on $uibModalStack.
It'll likely be located in \node-modules\angular-ui-boostrap\dist\ui-boostrap-tpls.js
and dismissAll() is # line 4349
Took me a while to find it.

Resources