In the previous post, we created a simple service called deregister to make event listeners deregistration easier. But sometimes you don’t even need to listen to an event more than once, and then the deregistration task is boring. No more!

Got the data? Move on, soldier.

Sometimes, you just want to listen to an event, but just one time. Your code usually ends up like this:

1
2
3
4
5
6
7
8
9
10
function ($rootScope) {
var dereg = $rootScope.$on('myEvent', doStuffOnce);

function doStuffOnce (event, data) {
// deregister the doStuffOnce listener
dereg();

doStuff(data);
}
}

This is boring. jQuery’s got .one for this kind of stuff (even jqLite does!). Why didn’t they implement Scope.$one, man I will never now. But thanks to AngularJS 1.4 decorators, we can fix this!

Enter Scope.$one

Turns out it’s quite easy to implement Scope.$one by decorating the $rootScope service. The Scope class itself isn’t exposed so we can’t alter its prototype, but we can add a hook to the $rootScope.$new method (which is used to create every single scope in the application) to inject our $one method afterwards.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
angular
.module('app')
.decorator('$rootScope', $rootScopeDecorator);

/* @ngInject */
function $rootScopeDecorator ($delegate) {
// Keep a reference to the original $rootScope.$new method, on the
// $rootScope object itself for simplicity so we can use this._$new
// later on.
$delegate._$new = $delegate.$new;
$delegate.$new = createNewScope;

// Now that we have patched $rootScope.$new to make sure every new
// scope gets the $one method, we need to inject it in the
// $rootScope itself.
$delegate.$one = one;

return $delegate;
}

function createNewScope (isolate, parent) {
// Call the original $rootScope.$new method
var newScope = this._$new(isolate, parent);

// Inject Scope.$one
newScope.$one = one;

return newScope;
}

function one (eventName, callback) {
// Scope.$on returns a deregistration function which we assign to
// the dereg variable...
var dereg = this.$on(eventName, function () {
// ... and call as soon as the event is fired.
dereg();

// Now we can call the original callback with received arguments
callback.apply(null, arguments);
});

// And we also return the deregistration function if for whatever
// reason we want to stop listening before an event is fired.
return dereg;
}

Conclusion

Now we can update our original use case:

1
2
3
4
5
6
7
8
9
function ($rootScope) {
// Yay! We can use the $one method!
$rootScope.$one('myEvent', doStuffOnce);

function doStuffOnce (event, data) {
// Listener is already deregistered at this time
doStuff(data);
}
}

Sidenotes

Note that this tip doesn’t exempt you from deregistering manually if the event is never fired. Using our deregister service, the example looks like this:

1
2
3
4
5
6
7
8
9
function ($rootScope, deregister) {
$scope.$on('$destroy', deregister([
$rootScope.$one('myEvent', doStuffOnce)
]));

function doStuffOnce (event, data) {
doStuff(data);
}
}