How to handle $stateChangeSuccess event if multiple states have same controllers - angularjs

I'm using a single controller in two states. If I navigate within these two states, and try to handle $stateChangeSuccess event, I'm facing an issue which is, in each state, the event is triggering twice.
$scope.$on('$stateChangeSuccess', function (ev, to, toParams, from, fromParams) {
// some code...
});
I know that this is a game of $scope, but how can I handle it. Thanks in advance...

I believe you should be watching $stateChangeSuccess on the $rootScope like this instead:
$rootScope.$on('$stateChangeSuccess' function() {
//code
};
If that's not the case, here is some good information about controller scope. From that, you should be able to watch the two events: $viewContentLoaded and $destroy
$scope.$on('$viewContentLoaded', function() {
console.log('Stuff loaded');
});
$scope.$on('$destroy', function() {
console.log('Destroyed');
})
From there you should be able to determine the scope issue.

Related

How to prevent $scope.$destroy?

I want to prevent $scope.$destroy event in my nested controller in my page. I see the event object in callback function has method preventDefault, but it's not working as I expect.
$scope.$on('$destroy', function(event) {
event.preventDefault(); // don't working!
});
It is possible to prevent $destroy event?
plnkr Example

How can I inject custom data like factory in the ui router stateChangeStart function

I am calling this:
$state.go("main.projects.selected.dates.day", { id: $state.params.id }, {reload: true});
I would really like to pass custom data from the state.go function to the stateChangeStart function. This is possible with the state params - which I could abuse to accept a date object - but then my url would not work anymore correctly...
How can I pass data from state.go to stateChangeStart? Just by hacking the rootScope.customData property?
$rootScope.$on('$stateChangeStart', function (ev, toState, toParams, fromState, fromParams, injectCustomFactory here)
{
// Run custom logic here on the injected custom factory
// then change the toParams.customData property by assigning the injected value to this property.
}
WHY do I have to run this logic in the stateChangeStart you might ask.
Its because only there I can check wether the user changed the url actively ($rootScope.customData is then falsy) or a state.go is done from outside and before I do a $rootScope.customData = MyValueLogic;
Inject it in the run (or wherever you're watching $stateChangeStart):
angular.module('lalala').run(function ($rootScope, factory) {
$rootScope.on('$stateChangeStart', function (e, toState, ...) {
factory.doSomething();
});
});

Deregistration of event handlers in Angular

I saw a piece of code in a controller recently that went something like:
.controller('foobar', ['$scope', '$rootScope', function($scope, $rootScope) {
var eventHandler = $rootScope.$on('some-event', function() {
...
});
// remove eventHandler
$scope.$on('$destroy', eventHandler);
}]);
Questions:
Is executing the eventHandler "deregistration" function on $scope's $destroy event necessary?
If yes, would executing the deregistration function on $scope's $destroy event have been necessary if 'some-event' was $on $scope instead of $rootScope?
How do I know when I need to execute a deregistration function? I understand detaching or unbinding events is common for cleanup in JavaScript, but what rules can I follow to know when to do this in Angular?
Any advice about understanding this snippet/"deregistration" would be much appreciated.
In the example above the destroy method is necessary. The listener is bound to the $rootscope which means that even after the controller gets $destroy-ed the listener is still attached to the dom through the $rootscope. Every time the controller is instantiated a new eventhandler will be created so without the destroy method you will have a memory leak.
However if you bind the listener to the controllers $scope it will get destroyed along with the controller as the $scope gets destroyed so the listener has no connection to the dom thus making it eligible for garbage collection
Event handlers are only deregistered on controller's $destroy event when it is on that controller's $scope.
The deregistering would be unnecessary if it's on $scope since that's handled for you by Angular.
Generally if it's not tied to instance of the individual element, controller, or service you are listening on then that is when you need to handle deregistering yourself.
A good example is a directive that registers event listeners on the $document:
var module = angular.module('test', []);
module.directive('onDocumentClick', function directiveFactory($document) {
return {
link: function (scope, element, attrs) {
var onDocumentClick = function () {
console.log('document clicked')
};
$document.on('click', onDocumentClick);
// we need to deregister onDocumentClick because the event listener is on the $document not the directive's element
element.on('$destroy', function () {
$document.off('click', onDocumentClick);
});
}
};
});

$rootScope.$on not getting called after broadcast

I have two controllers and want to notify one controller that some event has happened from the other controller. For this I am trying to use angular broadcast but have been unsuccessful. Please find below my code:
firstController.js
$rootScope.$on('xyz',function(){
alert('Called');
});
secondController.js
$rootScope.$broadcast('xyz');
Can someone please help in identifying what I am missing here?
Use either a combination of:
$rootScope.$broadcast();
$scope.$on();
// or
$rootScope.$emit();
$rootScope.$on();
$broadcast dispatches the event downward to all child scopes, so you can listen to it with the $scope service.
However, $emit dispatches upward through the scope hierarchy, and since $rootScope is the at the highest level, you can use $rootScope to dispatch and listen to the same event. This is also much better in regards to performance since the event doesn't propagate down through multiple scopes.
Please have a look here: jsfiddle
var app = angular.module('app', []);
app.controller('c1', function($rootScope, $scope){
$scope.click = function() {
$rootScope.$broadcast('xyz');
}
});
app.controller('c2', function($scope){
$scope.$on('xyz', function(){
alert("clicked");
});
})
in firstController.js use $scope instead $rootScope
$scope.$on('xyz', function(event, args) {
alert('Called')
});

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