Search code examples
angularjsangular-ui-routeraddeventlistener

Unbinding eventListener when a $state (ui-router) is changed


In order to make different components to talk to each other, I'm using the $emit method within a method of a service

Service code:

this.triggerSelectedAction = function( actionName )
{
    $rootScope.$emit('action-changed', { actionName: actionName } );
};

this.triggerSelectedProject = function( projectName )
{
    $rootScope.$emit('project-changed', { projectName: projectName } );
};

this.triggerSelectedFile = function( fileName )
{
    $rootScope.$emit('file-changed', { fileName: fileName } );
};

this.subscribe = function( scope, cb )
{
    var eventHandler;
    eventHandler = $rootScope.$on('action-changed', cb);
    eventHandler = $rootScope.$on('project-changed', cb);
    eventHandler = $rootScope.$on('file-changed', cb);

    scope.$on( '$destroy', eventHandler );
};

Now let's assume that from somewhere inside the app I'm calling the myService.triggerSelectedAction('doSomething'); and there is a specific $state (e.g toolsState) loaded at the time whose its $onInit method waits for this event to occur.

toolsState Controller $onInit code:

myService.subscribe($scope, function eventOccured(event, data) {
    if ( event.name === "action-changed" )              
        setEnvironment();
});

function setEnvironment() {
    // Attach an event listener to the select element
    document
        .getElementById("design-select")
        .addEventListener("change", projectSelectedEvent);
}

Now let's say that we change a state and go for example to the manageState which is also waiting for the same event to occur and has a similar $onInit code except the fact that it binds the eventListener to a different element.

manageState Controller $onInit code:

myService.subscribe($scope, function eventOccured(event, data) {
    if ( event.name === "action-changed" )              
        setEnvironment();
});

function setEnvironment() {
    // Attach an event listener to the select element
    document
        .getElementById("manage-select")
        .addEventListener("change", projectSelectedEvent);
}

In such a case, if the event is fired, everything works as expected except that I'm getting a TypeError: Cannot read property 'addEventListener' of null in the console that refers to the toolsState Controller line with the addEventListener method. It complains that it cannot find the element with id of design-select due to the state change.

To me, it seems that the $scope of the toolsState when the state changed and so its myService.subscribe() method still running.

Is there any way to get rid of this error.


Solution

  • Whenever code passes a function or object to an API, one risks creating object references which will prevent garbage collection. This can result in memory leaks.

    The $on method returns a function that can used to unbind the event handler:

    this.subscribe = function( scope, cb )
    {
        var deRegister = {};
        deRegister.ActionChanged = $rootScope.$on('action-changed', cb);
        deRegister.ProjectChanged = $rootScope.$on('project-changed', cb);
        deRegister.FileChanged = $rootScope.$on('file-changed', cb);
    
        scope.$on( '$destroy', function() {
            deRegister.ActionChanged();
            deRegister.ProjectChanged();
            deRegister.FileChanged();
            deRegister = null; //release deRegister object
            scope = null;      //release scope reference
            cb = null;         //release callback function reference
        ));
    };
    

    In addition to de-registering the event handlers, the code needs to release any object references created.