I am using the $broadcast pattern outlined here to controllers to listen to their service's state.
for example, here I listen to the service's busy/unbusy state to set the mouse cursor.
//Our service
function startRunning(){
$rootScope.$broadcast("busy");
}
function stopRunning(){
$rootScope.$broadcast("unbusy");
}
//Our controller
$scope.$on("busy", function(){
$scope.state = "busy";
});
$scope.$on("unbusy", function(){
$scope.state = "ready"
});
HTML
<div ng-controller = "myctrl" ng-state = "state"/>
CSS:
.busy{
cursor: wait;
}
.ready {
cursor:auto;
}
The problem with this is that the cursor doesn't change immediately. It usually requires me to move the mouse, I imagine to trigger the $digest cycle, before the cursor changes.
I can fix that with
$scope.$apply($scope.state = "ready");
But this will throw up:
Error: [$rootScope:inprog] $digest already in progress
errors.
What is the best way to deal with this?
Edit: here's a working JSFiddle: http://jsfiddle.net/HB7LU/23512/
The issue appears to be something to do with using non-angular timeout/asynchronous methods. (ie. issue doesn't appear if using a $timeout, but does appear if using a setTimeout;
Related
I'm working on a pretty big application in AngularJS and to avoid memory leaks we're implementing the memory release in the $onDestroy method, the problem is that there are variables that become undefined however, ng-change events keep coming from HTML and I have some errors. Is there any way to disconnect all the HTML from the controller? or at least to stop all the events coming from the frontend? I'm working in AngularJS 1.6.
This is an example of how I have defined the components:
function requestListController($uibModal, urlRest, $stateParams, $state, uiGridConstants, $filter, httpService) {
var ctrl = this;
ctrl.$onInit= function() {
// ALL DATA INITIALIZATION
ctrl.requestListGridOptions.data = [];
// GETTING EXTERNAL DATA
httpService.get(url, true)
.then(function(response){
console.log("initRequestList - data.RequestListTO : " , response.RequestListTO);
angular.copy(response.RequestListTO.requests, ctrl.requestListGridOptions.data);
}) .catch(function onError(response) {
// Handle error
var status = response.status;
console.log("initRequestList - error : " + status);
});
};
//////////////////////////////
// //
// on$Destroy method //
// //
//////////////////////////////
ctrl.$onDestroy = function() {
ctrl.status=undefined;
ctrl.requestListGridOptions=undefined;
};
// OTHER METHODS
};
//Inject dependencies
requestListController.$inject = [ '$uibModal', 'urlRest', '$stateParams', '$state', 'uiGridConstants', '$filter', 'httpService'];
pomeApp.component('requestList', {
templateUrl: 'request/requestList/requestList.template.html',
controller: requestListController
});
This is more less the structure of my components.
I guess you misinterpret the onDestroy event. It's mainly to remove timeouts or intervals or events for $rootScope.$on(...).
The ng-change event is bind to the scope. This means it will automatically destroyed if the scope is removed. Therefore the whole scope won't be destroyed and you have another problem.
If you have one big application with one scope or something similar you should use ng-if to remove the parts that should not be shown. This will remove the DOM element and with it all the watchers if the variable for ng-if is false.
Without any proper code from your side no one can really help you and just make some guesses what your problem could be.
You first need to see how many events have been subscribed. Then in the destroy, you can unsubscribe all those events. Sometimes, we also use directives which have to be destroyed. Or there is some logic inside those directives which needs cleanup. Also, if you have subscribed to any events on the root scope, it will live even after a local scope has been destroyed.
Im working on angularjs 1.4. Im trying to have some frontend-cache collection that updates the view when new data is inserted. I have checked other answers from here Angularjs watch service object but I believe Im not overwriting the array, meaning that the reference is the same.
The code is quite simple:
(function(){
var appCtrl = function($scope, $timeout, SessionSvc){
$scope.sessions = {};
$scope.sessions.list = SessionSvc._cache;
// Simulate putting data asynchronously
setTimeout(function(){
console.log('something more triggered');
SessionSvc._cache.push({domain: "something more"});
}, 2000);
// Watch when service has been updated
$scope.$watch(function(){
console.log('Watching...');
return SessionSvc._cache;
}, function(){
console.log('Modified');
}, true);
};
var SessionSvc = function(){
this._cache = [{domain: 'something'}];
};
angular.module('AppModule', [])
.service('SessionSvc', SessionSvc)
.controller('appCtrl', appCtrl);
})();
I thought that the dirty checking would have to catch the changes without using any watcher. Still I put the watcher to check if anything gets executed once the setTimeout function is triggered. I just dont see that the change is detected.
Here is the jsbin. Im really not understanding sth or doing a really rockie mistake.
You need to put $scope.$apply(); at the bottom of your timeout to trigger an update. Alternatively you can use the injectable $timeout service instead of setTimeout and $apply will automatically get called.
jsbin
Imagine I have a controller which handles, for example, view changes:
function Controller($scope){
var viewModel = this;
viewModel.goBack= function(){
viewModel.visible = visibleLinks.pop(); //get last visible link
viewModel.swipeDirection = 'left';// for view change animation
}
}
But I want to handle it not only for example with HTML buttons inside <body>, but also with Back button on device. So I have to add Event Listener for deviceready event, and also explicit call $scope.$apply() in order to fact, that it is called outside of AngularJS context, like this:
document.addEventListener("deviceready", function(){
document.addEventListener("backbutton", function(){
viewModel.goBack();
$scope.$apply();
}, false);
}, false);
}
But I also want to follow (relatively :) ) new controllerAssyntax, cause this is recommended now e.g. by Todd Motto: Opinionated AngularJS styleguide for teams and it allows to remove $scope from controllers when things like $emit or $on are not used. But I can't do it, case I have to call $apply() cause my context is not Angular context when user clicks on device back button. I thought about creating a Service which can be wrapper facade for cordova and inject $scope to this service but as I read here: Injecting $scope into an angular service function() it is not possible. I saw this: Angular JS & Phonegap back button event and accepted solution also contains $apply() which makes $scope unremovable. Anybody knows a solution to remove Cordova specific events outside Angular controller, in order to remove $scope from controllers when not explicity needed? Thank you in advance.
I don't see a reason why to remove the $scope from the controller. It is fine to follow the best practice and to remove it if not needed, but as you said you still need it for $emit, $on, $watch.. and you can add it $apply() in the list for sure.
What I can suggest here as an alternative solution is to implement a helper function that will handle that. We can place it in a service and use $rootScope service which is injectable.
app.factory('utilService', function ($rootScope) {
return {
justApply: function () {
$rootScope.$apply();
},
createNgAware: function (fnCallback) {
return function () {
fnCallback.apply(this, arguments);
$rootScope.$apply();
};
}
};
});
// use it
app.controller('SampleCtrl', function(utilService) {
var backBtnHandler1 = function () {
viewModel.goBack();
utilService.justApply(); // instead of $scope.$apply();
}
// or
var backBtnHandler2 = utilService.createNgAware(function(){
viewModel.goBack();
});
document.addEventListener("backbutton", backBtnHandler2, false);
});
In my case I was simply forwarding Cordova events with the help of Angular $broadcast firing it on the $rootScope. Basically any application controller would then receive this custom event. Listeners are attached on the configuration phase - in the run block, before any controller gets initialized. Here is an example:
angular
.module('app', [])
.run(function ($rootScope, $document) {
$document.on('backbutton', function (e) {
// block original system back button behavior for the entire application
e.preventDefault();
e.stopPropagation();
// forward the event
$rootScope.$broadcast('SYSTEM_BACKBUTTON', e);
});
})
.controller('AppCtrl', function ($scope) {
$scope.$on('SYSTEM_BACKBUTTON', function () {
// do stuff
viewModel.goBack();
});
});
Obviously in the $scope.$on handler you do not have to call $scope.$apply().
Pros of this solution are:
you'll be able to modify an event or do something else for the entire application before the event will be broadcasted to all the controllers;
when you use $document.on() every time controller is instantiated, the event handler stays in the memory unless you manually unsibscribe from this event; using $scope.$on cares about it automatically;
if the way a system dispatches Cordova event changes, you'll have to change it in one place
Cons:
you'll have to be careful when inheriting controllers which already have an event handler attached on initialization phase, and if you want your own handler in a child.
Where to place the listeners and the forwarder is up to you and it highly depends on your application structure. If your app allows you could even keep all the logic for the backbutton event in the run block and get rid of it in controllers. Another way to organize it is to specify a single global callback attached to $rootScope for example, which can be overriden inside controllers, if they have different behavior for the back button, not to mess with events.
I am not sure about deviceready event though, it fires once in the very beginning. In my case I was first waiting for the deviceready event to fire and then was manually bootstrapping AngularJS application to provide a sequential load of the app and prevent any conflicts:
document.addEventListener('deviceready', function onDeviceReady() {
angular.element(document).ready(function () {
angular.bootstrap(document.body, ['app']);
});
}, false);
From my point of view the logic of the app and how you bootstrap it should be separated from each other. That's why I've moved listener for backbutton to a run block.
I have a custom directive datepicker that uses scope.apply and works well. I cut out most of it to avoid cluttering the question, here is a simple version
appAdmin.directive("datepickerPss", ["$compile", "$parse", function ($compile, $parse) {
return {
$element.datepicker($scope.options).on("changeDate", function (ev) {
$scope.$apply(function () {
ngModel.$setViewValue(ev.date);
});
});
}
}]);
I have the custom datepicker in a modal, I simply want to initialize the value so in my controller I did the following at the top and had the "$digest already in progress" error
$scope.sDate = Date.now();
So reading up on this issue and the scope apply I changed it to the following in my controller
$timeout(function() {
$scope.sDate = Date.now();
});
However I still get the $digest in progress error. I'm not sure where to go from here. All the posts I have read have had their issues resolved by using $timeout.
Remove $scope.$apply and just use $timeout instead.
$element.datepicker($scope.options).on("changeDate", function (ev) {
$timeout(function () {
ngModel.$setViewValue(ev.date);
}, 0);
});
$scope.$apply starts a new $digest cycle on $rootScope, so calling it inside of your directive starts another $digest cycle while one is already occurring. By wrapping your call in $timeout, it'll wait until the previous $digest cycle finishes before applying your changes.
In addition, if you are trying to initialize a value AFTER you've already bound bound your change event in your directive, you could run into issues since the directive's digest cycle might still be in progress while your controller is being parsed and executed.
Please see the fiddle:
http://jsfiddle.net/HB7LU/4089/
On window resize if you check the console, $scope.showName keeps getting toggled between true and false as expected. However the view does not update. It remains with the initialized value of true.
From my understanding, the {{}} or ng-bind provides 1 way binding from controller to the view, so the value in the view should update when it changes in the controller.
What am I missing?
The $scope only binds to the view on $digest cycles - your event doesn't trigger a digest cycle since there was no action taken. You have to call $scope.$apply() to trigger a view update.
Be warned tho, $scope.$apply() can throw an error if a $digest cycle is already in progress.
You are missing a call to $scope.$apply() since the event is being handled outside of an angular context you need to call $scope.$apply() to trigger a digest which will updated any watchers http://jsfiddle.net/HB7LU/4091/
function MyCtrl($scope, $window) {
$scope.name = 'Timothy';
$scope.showName = true
$scope.nickname = 'Tim'
angular.element($window).bind('resize', function() {
if ($scope.showName){
$scope.showName = false;
}
else
{
$scope.showName = true;
}
console.log($scope.showName);
$scope.$apply();
});
}
You need to add $scope.$apply(); after any scope manipulation outside Angular events. Add it as the last statement of the bind function.
missing a call to $scope.$apply()
Look at http://jsfiddle.net/HB7LU/4093/
http://jsfiddle.net/HB7LU/4094/
$scope.$digest();
Try with this code..
I have add $digest of scope alter resize