Search code examples
angularjsangularjs-scopeangular-http-interceptors

Cannot catch event in AngularJS Controller


I'm trying to implement a user authentication system. I've read about 401 http status code and HTTP Auth Interceptor Module.

I've downloaded the module with bower and loaded it in my app module. I'm not using it yet but I want to use the same event that this module trigger to display the user login popup when the user open the application in the browser.

All my controller are children of the CoreController. The CoreController resolve the current user. When the application load there is not user logged in so I want to display the login form emitting the event: event:auth-loginRequired (same as http-auth-interceptor).

In this CoreController I have another rule which is listening for this specific event (event:auth-loginRequired). If this event is detected, the controller display the popup calling its state id.

The issue I have is that the console is saying that I've emitted the event correctly, but my listener is not triggered so the login popup is not showing.

Here is my CoreController:

/**
 * Index controller definition
 *
 * @scope Controllers
 */
define(['./../module', 'moment'], function (controllers, moment) {
    'use strict';

    controllers.controller('CoreController', ['$scope', '$rootScope', '$state', 'user', function ($scope, $rootScope, $state, user)
    {
        console.log('Core Controller Loaded');
        $scope.loader = true;

        $scope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams)
        {
            $scope.loader = true;
        });

        $scope.$on('event:auth-loginRequired', function(event, toState, toParams, fromState, fromParams)
        {
            console.log('catch auth required');
            $state.go("main.login");
        });

        if (user.isLogged == false) {
            console.log('emit auth required');
            $rootScope.$emit('event:auth-loginRequired');
        }
    }]);
});

What am I doing wrong?

Cheers, Maxime


Solution

  • The problem is that you are emitting on $rootScope and that event won't reach your local $scope listener.

    The easy way out (not recommended):

        if (user.isLogged == false) {
            console.log('broadcast auth required');
            $rootScope.$broadcast('event:auth-loginRequired');
        }
    

    The problem with this solution is that the event will bubble downwards from your $rootScope onto all of your child scopes. This is not good from a performance point of view.


    A better way out (recommended):

        $rootScope.$on('event:auth-loginRequired', function(event, toState, toParams, fromState, fromParams)
        {
            console.log('catch auth required');
            $state.go("main.login");
        });
    
        if (user.isLogged == false) {
            console.log('emit auth required');
            $rootScope.$emit('event:auth-loginRequired');
        }
    

    Here we're setting up the event listener on $rootScope and as we are $emitting from $rootScope - the event won't traverse through your $scope hierarchy and cause performance issues.


    Another way, and this is my prefered way of handling $rootScope event listeners from Controllers, is to unsubscribe from the event listener when the local $scope is destroyed.

    An even better way out (highly recommended (in my opinion*)):

    var mainModule = angular.module('myModule'); // this would be your core module, tying everything together. 
    
    mainModule.config(function ($provide) {
      // Define an alternative to $rootScope.$on. 
      // $scope.subscribe will emulate the same behaviour, with the difference
      // of removing the event listener when $scope.$destroy is called. 
    
      $provide.decorator('$rootScope', function ($delegate) {
        Object.defineProperty($delegate.constructor.prototype, 'subscribe', {
          value: function (name, listener) {
            var unsub = $delegate.$on(name, listener);
            this.$on('$destroy', unsub);
          },
          enumerable: false
        });
    
        return $delegate;
      });
    });
    
    // Then in your CoreController
    
     $scope.subscribe('event:auth-loginRequired', function(event, toState, toParams, fromState, fromParams)
      {
          console.log('catch auth required');
          $state.go("main.login");
      });
    
      if (user.isLogged == false) {
          console.log('emit auth required');
          $rootScope.$emit('event:auth-loginRequired');
      }
    

    What this also does, is that you no longer need to inject $rootScope into your controllers for setting up $rootScope listeners (this does not apply to setting up emitters though).

    Whichever way you decide to tackle this, I would highly suggest you look into solution #2 or #3. #1 is definitely not something I would recommend.

    Good luck!