$on only getting called on one controller - angularjs

I have a controller that has an event called changeSafe. I want other controllers to listen when this event gets fired
secure.controller('wrapperCtrl', function ($scope, $rootScope, storeFactory, safeFactory) {
$scope.changeSafe = function (safeId) {
$scope.loading = true;
safeFactory.setSafe(safeId).then(function (safeData) {
$scope.safe = safeData;
$scope.loading = false;
$rootScope.$broadcast('changeSafe', safeData);
});
}
});
The first page that loads is called dashboard when I add what is below the page re-draws with $scope.safe as I would expect it to.
secure.controller('dashboardCtrl', function ($scope, $rootScope, storeFactory, safeFactory) {
$scope.$on('changeSafe', function (event, arg) {
bindSafe(arg.safeId);
});
});
I have pretty much the exact same thing on my history Controller
secure.controller('historyCtrl', function($scope, $rootScope, storeFactory, safeFactory) {
$scope.$on('changeSafe', function (event, arg) {
bindHistory(arg.safeId);
});
});
Here is I have in the config section
secure.config(['$routeProvider', function ($routeProvider) {
$routeProvider
.when('/history', {
templateUrl: '/Angular/History/history.html',
controller: 'historyCtrl'
})
.otherwise({
templateUrl: '/Angular/Dashboard/dashboard.html',
controller: 'dashboardCtrl'
});
}]);
Whenever I click the button that is held within wrapperCtrl only the $scope.$on fires in the dashboardCtrl. Can anyone see why the $scope.$on is not being fired from the historyCtrl controller? I'm also unclear why it gets called from dashboardCtrl when I'm not on that view anymore.
When I step through the code, I'm actually seeing the $rootScope.$on('changeSafe') getting called multiple times both on history page and dashboard page. I can't figure out why it is changing views back to the dashboard though

I'm not sure if I have a full grasp of your problem, but my best guess is you are having a load order issue and the event is being broadcasted before the subscription has been initialized.
First off, to save yourself a lot of headaches, it is important to have a solid understanding of Event Handling in AnguarJS:
know the difference between subscribing on $rootScope.$on vs
$scope.$on
know the difference of publishing with $broadcast vs $emit on
$rootScope vs $scope
From the comments it sounds like you might have been using $rootScope.$on, which isn't cleaned up when your controllers are destroyed (same as directives):
var changeSafeListnerUnbindFunction = $rootScope.$on('changeSafe', funciton() { //... });
$scope.$on('$destroy', changeSafeListnerUnbindFunction);
Given your use case, registering listeners on the child scopes will pick up events published from the $rootScope.$broadcast (which communicates top level down to each child scope).
You probably have a load order issue?

Related

ngRoute infinite loop when changing path on a root event

I'm not sure why, but when I catch an event from JQuery, update the route and notify Angular of the change, it goes into an infinite route. It's easy to reproduce, say you have a screen like this:
The click of the button is handled by jQuery, because the jquery library doesn't seem to work in a directive so I put it in a service. Once the controller loads I initialize this. Then when the user clicks that button, I catch this jquery event, and raise a new event on the root scope so that I know to navigate to the new view. Here's the code:
(function(angular, $) {
var app = angular.module('app', ['ngRoute']);
app.config(['$locationProvider', '$routeProvider',
function config($locationProvider, $routeProvider) {
$routeProvider.when('/', {
templateUrl: 'list.html',
controller: 'ListController'
});
// The view I am trying to reach via jquery event.
$routeProvider.when('/Edit', {
templateUrl: 'edit.html',
controller: 'ListController'
});
$locationProvider.html5Mode(true);
}]);
app.controller('ListController',
function ListController($scope, $rootScope, $location, JQuerySerivce) {
// Initialize the button click listener.
JQuerySerivce.registerBtn();
$rootScope.$on('clicked', function() {
$location.path("./Edit");
// Notify angular stuff happened.
$scope.$apply();
});
});
app.controller('EditController', function EditController() {
console.log("Edit view loaded!");
});
app.factory("JQuerySerivce", function ($rootScope){
return {
registerBtn: function() {
$('#jqueryBtn').on('click', function() {
$rootScope.$broadcast('clicked');
});
}
};
});
})(angular, jQuery);
The working plunker example:
https://embed.plnkr.co/WIwRreYtofGKQWdjTeQx/
[EDIT]
To clarify my question, how can I prevent this infinite loop from happening? All of the other quesitons I've found concerning routing and infinite loops have usually been some sort of mistake that introduces a loop. In this case I'm just navigating and the framework is dying. Maybe my approach is wrong? I'm open to changing it if anyone has an alternative. I can't escape the jquery event though :/ Any help would be much appreciated.
$location.path("/Edit");
instead of
$location.path("./Edit");
?

Refreshing Resolve Data - Ui Router

In ionic I'm resolving my data with the Ui-Router's resolve functionality before the controller is initialized. As of now I don't have to $inject my EventService into the Controller. The EventService's getEvents() method resolves the data before the controller is initialized. Everything works correctly this way, but now i'm trying to implement the Ion Refresher. I could easily refresh my $scope.events array within the controller, bloating the controller itself, because I would have to $inject the EventService into the controller, and that also means that every controller that uses the same data will have to contain logic to handle a refresh. What is the best way refresh the data outside of the controller or is that the best way?
Events State Definition and data resolution
.state('tab.events', {
url: '/events',
views: {
'tab-event': {
templateUrl: 'views/events.html',
controller: 'EventsController',
resolve: {
events: function (EventService) {
return EventService.getEvents(); //resolves data before ctrl initialized
}
}
}
}
})
Events Controller
(function() {
'use strict'
angular
.module('app.events')
.controller('EventsController', EventsController);
EventsController.$inject = ['$scope','events'];
function EventsController ($scope,events) {
$scope.events = events;
}
}
)();
Bloated Events Controller - Example
(function() {
'use strict'
angular
.module('app.events')
.controller('EventsController', EventsController);
EventsController.$inject = ['$scope','events','EventsService'];
function EventsController ($scope,events,EventsService) {
$scope.events = events;
$scope.refresh = refresh;
function refresh () {
clearCache(); //pretend method
EventsService.getEvents()
.then(function (events) {
$scope.events = events;
$scope.$broadcast('scroll.refreshComplete');
})
}
}
}
)();
Rather than bloating the controller can I refresh this data another way?
call $state.reload() which is an alias for:
$state.transitionTo($state.current, $stateParams, {
reload: true, inherit: false, notify: true
});
This will cause all your states to be "re-entered" which fetches the resolves and re-initializes the controllers.
I wish a hard refresh, which is basically what a $state.reload() does wasn't the answer. I too have this issue and would rather be able to call some method that just forces all the resolved data objects to rerun. The reload causes a page refresh, which causes nasty UI artifacts.

UI-Router - scope not destroyed on state change?

I'm a new user to AngularJS and ui-router, and I'm trying to turn my head around on how the scope is managed. I was expecting the scope of an active controller would be destroyed when it becomes inactive on state change, however, it doesn't appear to be the case.
I've modified the example from UI-Router's website to illustrate the situation (see plunker below). Every time when the state route1.list/route2.list is triggered, they will emit an event on $rootScope. On receiving the event, a debug statement will be printed to console.
By toggling between the two states a few times, it is observed that all the controllers initialized previously responded the the event. So it appears that the scopes created by them have never been destroyed. Is this behavior expected? If so, what should I do such that only active controllers will respond to an event?
Plunker
Debug message printed on Console:
Code:
var myapp = angular.module('myapp', ["ui.router"])
myapp.config(function($stateProvider, $urlRouterProvider){
// For any unmatched url, send to /route1
$urlRouterProvider.otherwise("/route1")
here is the route1
$stateProvider
.state('route1', {
url: "/route1",
templateUrl: "route1.html"
})
.state('route1.list', {
url: "/list",
templateUrl: "route1.list.html",
controller: function($rootScope, $scope){
$rootScope.$emit("eventCT1");
$rootScope.$on("eventCT2", fn);
function fn () {
console.log("Controller 1 receives an event emitted by Controller 2");
}
$scope.items = ["A", "List", "Of", "Items"];
}
})
and here is route 2
.state('route2', {
url: "/route2",
templateUrl: "route2.html"
})
.state('route2.list', {
url: "/list",
templateUrl: "route2.list.html",
controller: function($rootScope, $scope){
$rootScope.$emit("eventCT2");
$rootScope.$on("eventCT1", fn);
function fn () {
console.log("Controller 2 receives an event emitted by Controller 1");
}
$scope.things = ["A", "Set", "Of", "Things"];
}
})
...
If we want to do something with
1) $rootScope inside of the controller (which has very limited lifetime),
2) we must destroy that, when controller (its $scope in fact) is being destroyed
So, this is the way how to hook and unhook
// get remove function
var removeMe = $rootScope.$on("eventCT2", ...);
// call that function
$scope.$on("$destroy", removeMe)
But, in the case above, we should not even try to
1) create some controller action for one state...
2) and expect it will be called in another controller from different state
These will never live together
If you are using Ionic with Angular, you could use the life cycle events like so:
$scope.$on("$ionicView.beforeEnter", function(){
//Do something every time this controller is the active scope.
})
You could play around with the other events provided in the above link as well. And it's probably best practise to minimize the use of $emit, which will lead to more predictable code and fewer state mutations.

Angular multiple instances of the same controller are created

In my example below you can see that each time you go to a page a new instance of the same controller is created with a different id, and the old one is not destroyed, the setIntervat method keeps logging the corresponding scope id and fruit name on all of them. Let's say I have an ajax call instead of the log, that refreshes the page's content from time to time, but I don't want to make calls for the inactive pages, how can I fix this?
example
var app = angular.module('plunker', ['ngRoute']);
app.config(['$routeProvider', function($routeProvider) {
$routeProvider
.when('/fruit/:fruitId', {
templateUrl: 'fruit-tpl.html',
controller: 'FruitController'
})
.otherwise({
redirectTo: '/fruit/1'
});
}]);
app.controller('FruitController', ['$scope', '$routeParams',
function($scope, $routeParams) {
var fruitArr = ['Lemons', 'Oranges', 'Grapefruit'],
fruitId = $routeParams.fruitId - 1;
$scope.fruitName = fruitArr[fruitId];
setInterval(function() {
console.log('scope', $scope.$id, $scope.fruitName);
}, 3000);
}
]);
setInterval does not get stopped automatically when a controller is destroyed, and until it is stopped, the variables closed on by the closure you pass to it will remain in memory. Even if you switched to $interval, the same problem would persist.
Note: Intervals created by this service must be explicitly destroyed when you are finished with them. In particular they are not automatically destroyed when a controller's scope or a directive's element are destroyed. You should take this into consideration and make sure to always cancel the interval at the appropriate moment. See the example below for more details on how and when to do this.
var theInterval = setInterval(function() {
console.log('scope', $scope.$id, $scope.fruitName);
}, 3000);
$scope.$on('$destroy', function() {
// Make sure that the interval is destroyed too
clearInterval(theInterval);
});

How to call controller's event after async treatments on the run part?

I look for a user's session in the run process, and i would like to open a modal if i got a specific information from the User.
The thing is i need to send the scope to the modal, so i have to open it from a controller.
The problem is, when angular load controller, the user is not logged yet.
I tried to $location.path('/'); after the run async process, but it doesn't call the indexCtrl twice (because i am already on the view he is binding).
So my question is, how could i "refresh" my controller at the User.getSession() callback or how could i approach the thing differently ?
angular.module('TEST').run(function($rootScope, $window, AuthenticationManager, $location) {
AuthenticationManager.getSession(function() {
$location.path('/'); //Unfortunately useless because it doesn't call the indexCtrl twice
});
};
angular.module('TEST').controller('indexCrtl', function($scope, $User) {
if ($User.nb_points === 10) //Not fired because the user is not logged at the first load
var Modal = $modal({scope: $scope, show: false, template: 'modal.html'});
};
I think you are looking for $route.reload()
Causes $route service to reload the current route even if $location
hasn't changed.
As a result of that, ngView creates new scope, reinstantiates the
controller.
Example on jsfiddle

Resources