9
votes

I have a main state which is reached by accessing the URL http://myapp/validate.

Upon reaching this URL, inside the controller I do a permission check, and redirect the user to one of the two child states.

This works, 80% satisfied, but uses some resources and causes a small flicker in my app, since basically I load a view I DO NOT USE.

Is there a better way to achieve this?

Any ideas?

angular.module('app.validate').config(['$stateProvider', function($stateProvider) {
    $stateProvider
        .state('validate', {
            url: '/validate',
            template: '<ui-view/>',
            controller: ['$state', 'HasPermission', function ($state, HasPermission) {

                // Here is where I do my logic.

                if (HasPermission.get('can-lock-as-admin')) {
                    $state.go('validate.lock-as-admin');
                }

                if (HasPermission.get('can-lock-as-clerk')) {
                    $state.go('validate.lock-as-clerk');
                }
            }]
        })
        .state('validate.lock-as-admin', {
            templateUrl: 'theUrl',
            controller: 'ValidateLockAdminCtrl'
        })
        .state('validate.lock-as-clerk', {
            templateUrl: 'theUrl',
            controller: 'ValidateLockClerkCtrl'
        })
        .state('validate.commit', {
            templateUrl: 'theUrl',
            controller: 'ValidateCommitCtrl'
        });
}]);
3
I think you need to get rid of: template: '<ui-view/>', as rendering this empty view may be causing the flicker. - Nitin...
Let me check, I am not sure, but if there isn't any '<ui-view>' child states won't work. - Dany D
Yes,. child states need a <ui-view/> on the parent - Dany D
Yes, this is generally a case when named states views are useful. - Nitin...
I've been searching the net high and low for the ui-router to have the possibility of loading states based on a certain condition, with no success. - Dany D

3 Answers

7
votes

The flickering is due to the fact that you are actually letting the user access http://myapp/validate before moving him to the correct path.
You can avoid this by redirecting him to the correct path without displaying the parent route.

app.run(function($rootScope){

  $rootScope.$on('$stateChangeStart', 
    function(e, toState, toParams, fromState, fromParams), {
      if(toState.name === 'validate') { // some conditional
        e.preventDefault(); // This tells the app not to move to the validate route

        if (HasPermission.get('can-lock-as-admin')) {
           $state.go('validate.lock-as-admin');
        }
        if (HasPermission.get('can-lock-as-clerk')) {
           $state.go('validate.lock-as-clerk');
        }
      }
    });
  });

});  

To have an even cleaner solution you could probably use a resolve block that check the permission, but I am not 100% about this and I don't have time to check it now.
I would suggest you to have a look at this article since it explains in a good way similar scenarios.

0
votes

If you don't want to extend ui-router, I'd advise the following:

  • Don't create transitional states.

    The ui-router engine operates under the assumption that when you direct to a state and the transition is successful, the state's view should be rendered. Therefore, if you direct to a state and allow it to be resolved while actually trying to transition somewhere else, be prepared for flicker.

    This leads to the question "What if I don't let the state resolve and instead redirect?". Unfortunately, there seem to be some issues with redirection while in the middle of state resolution. See the part of the answer following the break.

  • Either create fallbacks ...

    Instead of having a separate transitional state, you can decide, which of the sub-states is the default and direct to that one. There you can perform your checks and possibly redirect somewhere else. This will still result in flicker in case of redirection. But if your default is well-chosen and the redirect happens only ocasionally, it's a good start.

  • ... or direct to correct destinations.

    Using the state machine properly means directing to states where you actually want the user to go. This means that any checks should be performed before triggering state transitions. Of course, this is only possible when directing programatically. You still need the previous "fallback" strategy to deal with user trying to start in a specific state by entering its URL.


Alternatively, you could extend the ui-router a bit to allow state transitions during state resolution. (The last time I checked, you couldn't safely redirect to different state from a resolve.) For example, you could decorate $state and stop redirection attempts while state resolution is already in progress (you can tell by observing appropriate events) and then redirect to the last caught state after state resolution completes. If you do that, you can create transitional states like this:

.state('someTransitionalState', {
    resolve: {
        redirect: ['permissions', '$state', '$q', function (permissions, $state, $q) {
            $state.go(permissions.has(...) ? ... : ...);
            return $q.reject('transitional state - redirected elsewhere');
        }]
    }
})

This way the transitional state will never resolve and all attempts to reach it will result in reaching the other selected destinations.

0
votes

You could use some custom permission attribute on routes, that you can check, like:

.state('validate.lock-as-admin', {
            template: '<h1>lock as admin</h1>',
            permission: 'admin',
            otherwise: 'validate.lock-as-clert'
        })
        .state('validate.lock-as-clerk', {
            template: '<h1>lock as clerk</h1>',
            permission: 'clerk',
            otherwise: 'validate.lock-as-admin'
        })

Then you could watch the routechanges and do some custom permission check. Here is an example, how this could be achieved. example