Search code examples
javascriptangularjsangular-ui-routerangular-decorator

How to decorate current state resolve function in UI-Router? Current function isn't invoked


I'm trying to DRY in $stateProvider and prevent adding the same auth function in each resolve. I've created decorator that in each state change would add this function to current state, but auth function isn't invoked, How to fix it or how to workaround discussed issue?

app.config(function ($stateProvider, $urlRouterProvider, $provide) {
  $provide.decorator('$state', function($delegate, $rootScope) {
    $rootScope.$on('$stateChangeStart', function(event, state, params) {
      if ($delegate.current === "login" || $delegate.current === "register") {
        return;
      }
      console.log("decorator", $delegate);
      $delegate.current.resolve = {
        auth: ['AuthService', '$stateParams', function(AuthService, $stateParams) {
          //how to invoke this function?
          if (AuthService.isAuthenticated()) {
            return AuthService.me(); //promise
          } else {
            return false;
          }
        }]
      };
    });
    return $delegate;
  });

states definition:

  $stateProvider.state('root', {
    abstract: true,
    url: '/',
    views: {
      "": {
        controller: 'RootCtrl',
        templateUrl: 'views/root.html'
      },
      "header@root": {
        templateUrl: 'views/header.html'
      }
    }
  })
  .state('root.home', {
    url: urlPrefix,
    views: {
      "content@artworks": {
        templateUrl: 'views/home.html',
        //resolve: {
        //  auth: ['AuthService', '$stateParams', function(AuthService, $stateParams) {
        //  }]
        //}
      }
    }
  })
  ...

Solution

  • If I understand your requirement correctly, we can use native UI-Router built-in decorator:

    decorator(name, func)

    Allows you to extend (carefully) or override (at your own peril) the stateBuilder object used internally by $stateProvider. This can be used to add custom functionality to ui-router, for example inferring templateUrl based on the state name... (read more in the doc)

    There is a working plunker

    So, we can have this var auth

    var auth = ['AuthService', '$stateParams',
      function(AuthService, $stateParams) {
        //how to invoke this function on needed states?
        if (AuthService.isAuthenticated()) {
          return AuthService.me();
        } else {
          return false;
        }
      }
    ];
    

    And here we just use decorator with some "IF" logic

    .config(['$stateProvider', 
      function($stateProvider) {
    
        $stateProvider.decorator('views', function(state, parent) {
          var result = {},
            views = parent(state);
    
          // some naive example when to not inject resolve
          if (state.name === "home") { 
            return views;
          }
          // child already has that in parent
          if (state.name.indexOf(".") > 0) { 
            return views;
          }
    
          angular.forEach(views, function(config, name) {
    
            // here inject the resolve (if not existing)
            config.resolve = config.resolve || {};
            // and extend it with the auth stuff above
            config.resolve.auth = auth;
    
            result[name] = config;
          });
    
          return result;
        });
    
      }
    ])
    

    And later few our states, which will be extended by the above stuff

    $stateProvider
        .state('home', {
          url: "/home",
          templateUrl: 'tpl.html',
        })
        .state('parent', {
          url: "/parent",
          templateUrl: 'tpl.html',
          controller: 'SharedCtrl',
        })
        .state('parent.child', {
          url: "/child",
          templateUrl: 'tpl.html',
          controller: 'SharedCtrl',
        });
    

    Check it in action here