So I have this situation where one controller is emitting the event and the other controller has the listener. Here is the code:
In controller A, I have this method:
$scope.process = function () {
var taskName = 'process';
$scope.$emit('process', taskName);
}
In controller B, I have this:
$rootScope.$on('process', function (event, taskName) {
//Do something here
});
Now whenever I visit other pages on application and comeback to this, the process listener gets created twice. I cannot use controller scope as the event is getting emitted from other controller. How can I destroy listener once it has completed its task? I have also tried $scope.$destroy(). Doesn't really work. What is the correct way of doing this?
I am on Angularjs 1.4.7.
Usually you do it in different way:
$rootScope.$broadcast(...)
...
$scope.$on(...)
Then you do not need to unsubscribe.
If you really need for some reason to subscribe to $rootScope, then:
var deregister = $scope.$on(...);
...
deregister(); // destory that listener
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.
Angular, UI-router. Using $interval in a controller of a state like so:
$scope.Timer = null;
$scope.startTimer = function () {
$scope.Timer = $interval($scope.Foo, 30000);
};
$scope.stopTimer = function () {
if (angular.isDefined($scope.Timer)) {
$interval.cancel($scope.Timer);
}
};
The problem? The timer persists upon leaving the state. My understanding was that the $scope and the controller are essentially "destroyed" when a state is left. So, based on that, the timer should stop (Within the controller, I am cancelling the timer when moving around, that works - but it persists if I navigate to a diff state). What am I misunderstanding here?
I guess since interval and timeout are services in angular, they are available everywhere, but I still don't understand how they see functions in the not-initialized controller, unless it's copied. Is my solution to just use regular good-old js interval?
clear interval on $destroy
Like this
$scope.$on("$destroy",function(){
if (angular.isDefined($scope.Timer)) {
$interval.cancel($scope.Timer);
}
});
Does anyone know how to prevent the event from firing twice? I've tried using a controller scope level boolean variable to see if the event has already fired, but it did not work. It is like the event is firing on 2 separate threads and the variable was always false.
In the code below the $ionicPlatform.ready event is firing twice, but I can't figure out why.I'm using the current version of the Ionic Framework ionic-v1.0.0-beta.13.
angular.module('rsgApp.controllers', [])
.controller('MapCtrl', ['$ionicPlatform',
function ($ionicPlatform) {
var vm = this;
$ionicPlatform.ready(function () {
alert('device is ready');
});
}]);
Thanks TechMa9iac I was able to resolve this problem. In my tab template I had added an 'ng-controller' attribute to my ion-content tag. This is what was causing the $ionicPlatform.ready event to fire twice.
I am looking for a way to execute code when after I add changes to a $scope variable, in this case $scope.results. I need to do this in order to call some legacy code that requires the items to be in the DOM before it can execute.
My real code is triggering an AJAX call, and updating a scope variable in order to update the ui. So I currently my code is executing immediately after I push to the scope, but the legacy code is failing because the dom elements are not available yet.
I could add an ugly delay with setTimeout(), but that doesn't guarantee that the DOM is truly ready.
My question is, is there any ways I can bind to a "rendered" like event?
var myApp = angular.module('myApp', []);
myApp.controller("myController", ['$scope', function($scope){
var resultsToLoad = [{id: 1, name: "one"},{id: 2, name: "two"},{id: 3, name: "three"}];
$scope.results = [];
$scope.loadResults = function(){
for(var i=0; i < resultsToLoad.length; i++){
$scope.results.push(resultsToLoad[i]);
}
}
function doneAddingToDom(){
// do something awesome like trigger a service call to log
}
}]);
angular.bootstrap(document, ['myApp']);
Link to simulated code: http://jsfiddle.net/acolchado/BhApF/5/
Thanks in Advance!
The $evalAsync queue is used to schedule work which needs to occur outside of current stack frame, but before the browser's view render. -- http://docs.angularjs.org/guide/concepts#runtime
Okay, so what's a "stack frame"? A Github comment reveals more:
if you enqueue from a controller then it will be before, but if you enqueue from directive then it will be after. -- https://github.com/angular/angular.js/issues/734#issuecomment-3675158
Above, Misko is discussing when code that is queued for execution by $evalAsync is run, in relation to when the DOM is updated by Angular. I suggest reading the two Github comments before as well, to get the full context.
So if code is queued using $evalAsync from a directive, it should run after the DOM has been manipulated by Angular, but before the browser renders. If you need to run something after the browser renders, or after a controller updates a model, use $timeout(..., 0);
See also https://stackoverflow.com/a/13619324/215945, which also has an example fiddle that uses $evalAsync().
I forked your fiddle.
http://jsfiddle.net/xGCmp/7/
I added a directive called emit-when. It takes two parameters. The event to be emitted and the condition that has to be met for the event to be emitted. This works because when the link function is executed in the directive, we know that the element has been rendered in the DOM. My solution is to emit an event when the last item in the ng-repeat has been rendered.
If we had an all Angular solution, I would not recommend doing this. It is kind of hacky. But, it might be an okey solution for handling the type of legacy code that you mention.
var myApp = angular.module('myApp', []);
myApp.controller("myController", ['$scope', function($scope){
var resultsToLoad = [
{id: 1, name: "one"},
{id: 2, name: "two"},
{id: 3, name: "three"}
];
function doneAddingToDom() {
console.log(document.getElementById('renderedList').children.length);
}
$scope.results = [];
$scope.loadResults = function(){
$scope.results = resultsToLoad;
// If run doneAddingToDom here, we will find 0 list elements in the DOM. Check console.
doneAddingToDom();
}
// If we run on doneAddingToDom here, we will find 3 list elements in the DOM.
$scope.$on('allRendered', doneAddingToDom);
}]);
myApp.directive("emitWhen", function(){
return {
restrict: 'A',
link: function(scope, element, attrs) {
var params = scope.$eval(attrs.emitWhen),
event = params.event,
condition = params.condition;
if(condition){
scope.$emit(event);
}
}
}
});
angular.bootstrap(document, ['myApp']);
Using timeout is not the correct way to do this. Use a directive to add/manipulate the DOM. If you do use timeout make sure to use $timeout which is hooked into Angular (for example returns a promise).
If you're like me, you'll notice that in many instances $timeout with a wait of 0 runs well before the DOM is truly stable and completely static. When I want the DOM to be stable, I want it to be stable gosh dang it. And so the solution I've come across is to set a watcher on the element (or as in the example below the entire document), for the "DOMSubtreeModified" event. Once I've waited 500 milliseconds and there have been no DOM changes, I broadcast an event like "domRendered".
IE:
//todo: Inject $rootScope and $window,
//Every call to $window.setTimeout will use this function
var broadcast = function () {};
if (document.addEventListener) {
document.addEventListener("DOMSubtreeModified", function (e) {
//If less than 500 milliseconds have passed, the previous broadcast will be cleared.
clearTimeout(broadcast)
broadcast = $window.setTimeout(function () {
//This will only fire after 500 ms have passed with no changes
$rootScope.$broadcast('domRendered')
}, 500)
});
//IE stupidity
} else {
document.attachEvent("DOMSubtreeModified", function (e) {
clearTimeout(broadcast)
broadcast = $window.setTimeout(function () {
$rootScope.$broadcast('domRendered')
}, 500)
});
}
This event can be hooked into, like all broadcasts, like so:
$rootScope.$on("domRendered", function(){
//do something
})
I had a custom directive and I needed the resulting height() property of the element inside my directive which meant I needed to read it after angular had run the entire $digest and the browser had flowed out the layout.
In the link function of my directive;
This didn't work reliably, not nearly late enough;
scope.$watch(function() {});
This was still not quite late enough;
scope.$evalAsync(function() {});
The following seemed to work (even with 0ms on Chrome) where curiously even ẁindow.setTimeout() with scope.$apply() did not;
$timeout(function() {}, 0);
Flicker was a concern though, so in the end I resorted to using requestAnimationFrame() with fallback to $timeout inside my directive (with appropriate vendor prefixes as appropriate). Simplified, this essentially looks like;
scope.$watch("someBoundPropertyIexpectWillAlterLayout", function(n,o) {
$window.requestAnimationFrame(function() {
scope.$apply(function() {
scope.height = element.height(); // OK, this seems to be accurate for the layout
});
});
});
Then of course I can just use a;
scope.$watch("height", function() {
// Adjust view model based on new layout metrics
});
interval works for me,for example:
interval = $interval(function() {
if ($("#target").children().length === 0) {
return;
}
doSomething();
$interval.cancel(interval);
}, 0);