How to improve eslint-plugin-angular on-watch error? - javascript

$rootScope.$on('$stateChangeStart', function (event, state) {
...
})
result -> es lint error The "$on" call should be assigned to a variable, in order to be destroyed during the $destroy event
but if i correct it like in the documentation
var unregister = $rootScope.$on('$stateChangeStart', function (event, state) {
...
})
i get "unregister is defined but never used" error
What is the best way to correct this issue?

The unregister variable that is returned by $rootScope.$on is a function that needs to be called to unregister the watch. A common use case is to call it when the current scope is destroyed:
$scope.$on('$destroy', unregister);

Related

AngularJS $watch unbind function is not defined when being called only

I am building a user session activity timer using AngularJS 1.6. I am using $scope.$watch to monitor the value of a countdown timer. This works for the most part although $watch is binding to the variable multiple times, every time the session timer function is called. To solve this, I think I need to simply unbind the watcher if it is already defined.
I saw some very simple examples on how to do this on SO, however nothing seems to be working. The $scope.$watch method returns a function that when called, unbinds the watcher.
However, anytime the unbind function is called, there is an error in the console.log 'ReferenceError: sessionWatchUnbind is not defined'. This seems very strange because the undefined error is happening within an IF statement that checks if the function is defined before calling it. How could it be defined and then undefined immediately after?
// If session countdown watch is already set unbind it to prevent multiple watches on the same value
if (sessionWatchUnbind) {
console.log('sessionWatchUnbind ', sessionWatchUnbind); // This prints the function definition to the console log properly
sessionWatchUnbind(); // This line throws an error 'ReferenceError: sessionWatchUnbind is not defined' but it was defined in the previous lines!?
}
// Set a watcher on session countdown value
sessionWatchUnbind = $scope.$watch('session_countdown', function (newValue, oldValue) {
if (newValue !== oldValue) {
// continued...
}
});
Also, the same error happens if I try placing the call inside the body of the watcher as such.
// Set a watcher on session countdown value
sessionWatchUnbind = $scope.$watch('session_countdown', function (newValue, oldValue) {
if (newValue !== oldValue) {
sessionWatchUnbind(); // This line throws an error ReferenceError: sessionWatchUnbind is not defined
// continued...
}
});
I am pretty sure my code is following the numerous examples on how to do this. Perhaps someone could point me in the right direction!
UPDATE:
I got this to work by binding sessionWatchUnbind to 'this'. Perhaps because the outer wrapper function is bound to 'this' as well. This solution was based on the accepted answer here: How to prevent/unbind previous $watch in angularjs
vm = this;
vm.startActivity = function () {
// If session countdown watch is already set unbind it to prevent multiple watches on the same value
if (vm.sessionWatchUnbind) {
vm.sessionWatchUnbind();
}
// Set a watcher on session countdown value
vm.sessionWatchUnbind = $scope.$watch('session_countdown', function (newValue, oldValue) {
console.log('sessionWatchUnbind newValue, oldValue ', newValue, oldValue);
if (newValue !== oldValue) {
//continued...
}
});
This code works well for me:
const watcher = $scope.$watch('variable', value => {
// Remove watcher
if (value)
watcher();
});

Wait for stateChange in test

I have a scope function in my controller with some async functions. After everything is done it changes the state.
controller.js
$scope.test = function () {
async().then(function () {
$state.go('somewhere');
});
};
I could test it with setTimeout() but thats dirty I think.
How do I wait for the stateChange in my test?
Edit:
I want to write unit tests for the test() function but as it is no promise I need to watch for the state change in the test. But how? It works with setTimeout() but I don't want to use setTimeout() because it just doesn't feel right. Is there something like $scope.$watch for states?
test.js
...
it('test()', function (done) {
$scope.test();
setTimeout(function () { // I want this replaced with a listener for state
expect($scope.someVar).to.be.equal('value');
expect($state.current.name).to.be.equal('somewhere');
});
});
...
As I was editing the question to describe my problem I found the solution.
It's possible to listen for broadcasted events so it's possible to use
...
it('test()', function (done) {
$scope.test();
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
expect($scope.someVar).to.be.equal('value');
expect(toState.name).to.be.equal('somewhere');
});
});
...
You can use
$q.all.(promises).then(
function(){
$state.go()
});
To wait for your tasks to finish, as long as you can wrap them in a promise.
More info here.

Notify promise angular remove callback

I would like to notify multiple controllers of external changes from a service.
I know that i can do this by using a deferred object and calling notify on it and registering a callback on that.
e.g
// in service
$timeout(function() {
defered.notify('In progress')}
, 0)
//in controller
var promise = myService.promise
promise.then(function(success) {
console.log("success");
}, function(error) {
console.log("error");
}, function(update) {
console.log("got an update!");
}) ;
Is there a way to remove my notify callback when the controller is destroyed?
You can use $on, $broadcast and $emit to achieve a similar behavior.
Just $broadcast(name, args); an event and register a listener $on(name, listener);.
So when you got your data you just broadcast to everybody that you've got it.
$broadcast('gotTheData', actualDataObject);
Then in your controllers
$on('gotTheData', function(event, actualDataObject)
{
... update controller data ...
});
Note that in the example I've added only one actualDataObject, but you can pass multiple arguments from the $broadcast to $on. For more details refer to the documentation (see link above).
As per the comments to #Scott's answer you highlighted one important aspect: you need to unregister the $on when you destroy the controller through the "Returns a deregistration function for this listener." (see the docs) at $on returns. There is a question that targets specifically this issue: How can I unregister a broadcast event to rootscope in AngularJS?.
EDIT DUE TO FURTHER QUESTION DETAILS
You could cancel the $timeout when you destroy the controller:
Create the $timeout as so...
$scope.myTimeout = $timeout(function() {
defered.notify('In progress')}
, 0);
In the controller add this:
$scope.$on('$destroy', function () {
$timeout.cancel($scope.myTimeout);
});
In my experience this appears to clean everything up.
To clear just the notify callback itself, use the suggested $broadcast, $emit(see #Pio's answer) A guide to the difference between them is here.
Its important to know the difference!

Does $scope.$on('$destroy', ...)'s event handler get destroyed?

Ran across a very "down-the-rabbit-hole" Angular question today I couldn't find the answer to. From the $scope docs, you can register an event handler on "$destroy", which is called right before a scope's destruction. That way, you can deregister event handlers like so:
var deregister = $scope.$on('myCustomEvent', function () {
// do some crazy stuff
});
$scope.$on('$destroy', function () {
deregister();
});
However, the $scope.$on('$destroy', ...) must create its own handler. Is that automatically destroyed, or do you have to do something like the following to destroy it?
var deregister = $scope.$on('myCustomEvent', function () {
// do some crazy stuff
});
var deregisterDestroy = $scope.$on('$destroy', function () {
deregister();
deregisterDestroy();
});
The answer is actually "maybe" depending on what you mean by it being automatically destroyed. If we look at the source for the $destroy method for scopes, we can see that while a $destroy event is broadcasted downward throughout child scopes, the actual $destroy method is never invoked on any scope but the initial one. That means that the actual cleanup and nulling out of properties never occurs on child scopes.
The reason that this doesn't leak memory is because once $destroy has been invoked on a scope, it becomes detached from the parent scope and is therefore eligible for garbage collection since it should no longer have any path to the GC Roots. This same logic applies to all child scopes since they also should have no paths to the GC Roots.
Your example is safe though; I do that myself in the same manner to clean up my own handlers when necessary and do not run into any kind of infinite loops.

How can mocha know when event fired on a Controller $scope using $broadcast inside of a $promise?

I could not find any other question/answer that met my needs, so here it is:
In an AngularJS (1.2.14) controller, I have an event listener that executes an ajax call to fetch some data when an event ('fetchData') is heard. After the fetch is successful, an event (called 'fetchSuccess') is broadcasted, that a directive is listening for (though that part is irrelevant).
$scope.$on('fetchData', function () {
$scope
.submitSearch()
.then(function (results) {
$scope.$broadcast('fetchSuccess');
}, function () {
$scope.$broadcast('fetchError');
});
});
So, in my test I want to do something like this (assume that 'this.scope' is a new $scope object on the controller in the test suite):
it('should broadcast "fetchSuccess"', inject(function ($rootScope) {
var scope = this.scope,
spy = chai.spy(scope, '$broadcast');
// trigger the $broadcast event that calls the fetch method
scope.$broadcast('fetchData');
$rootScope.$apply();
expect(scope.$broadcast).to.be.called.with('fetchSuccess');
}));
But I am not clear on how to listen for the $broadcast event in an assertion. I keep getting this error: AssertionError: expected function (name, args) {...}
Just to be clear, my issue is not with the functionality of the event broadcaster or the listeners during runtime; the application works as expected. The problem is with listening to the events in the test suite.
Note that the above code is just the necessary snippet that is needed for this question. In my application, there are other variables/methods that get set/called and those things test out correctly. Meaning that if I test to see if the actual fetching method gets called, or if a particular variable is being set appropriately, those tests pass.
I have tried mixing and matching the scope variables and even listening for the $broadcast via scope.$on('fetchSuccess', fn) but nothing seems to work. The fetchSuccess event doesn't seem to get emitted, or I'm not listening for it properly.
Thanks in advance,
Ryan.
So, I have found the answer to my question and it was all my fault. Though I did have to modify the way the test was written, the core problem as simple as listening for the wrong event!
But, for those that want to know what my test looked like, here is the final test (rootscope is being set to $rootScope elsewhere):
it('should broadcast a "fetchSuccess" event', function (done) {
var eventEmitted = false;
this.scope.$on('fetchSuccess', function () {
eventEmitted = true;
done();
});
this.scope.$broadcast('fetchData');
rootscope.$apply();
eventEmitted.should.be.true;
});
There was no need to spy on the $broadcast event, the AngularJS $on listener is sufficient.

Categories