32

In an angular app I'm working on, I'd like there to be an abstract parent state which must resolve certain dependencies for all of its children's states. Specifically, I'd like all states requiring an authenticated user to inherit that dependency from some authroot state.

I'm running into issues having the parent dependency not always being re-resolved. Ideally, I'd like to have the parent state check that the user is still logged in for any child state automatically. In the docs, it says

Child states will inherit resolved dependencies from parent state(s), which they can overwrite.

I'm finding that the parent dependency is only being re-resolved if I enter any child state from a state outside the parent, but not if moving between sibling states.

In this example, if you move between states authroot.testA and authroot.testB, the GetUser method is only called once. When you move to the other state and back, it will get run again.

I am able to put the User dependency on each of the child states to ensure the method is called every time you enter any of those states, but that seems to defeat the purpose of the inherited dependency.

Am I understanding the docs incorrectly? Is there a way to force the parent state to re-resolve its dependencies even when the state changes between siblings?

jsfiddle

<!doctype html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.1/angular.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.0/angular-ui-router.min.js"></script>
<script>
(function(ng) {
    var app = ng.module("Test", ["ui.router"]);
    app.config(["$stateProvider", "$urlRouterProvider", function(sp, urp) {
        urp.otherwise("/testA");
        sp.state("authroot", {
            abstract: true, 
            url: "",
            template: "<div ui-view></div>", 
            resolve: {User: ["UserService", function(UserService) {
                        console.log("Resolving dependency...");
                        return UserService.GetUser();
                    }]}
        });
        sp.state("authroot.testA", {
            url: "/testA", 
            template: "<h1>Test A {{User|json}}</h1>", 
            controller: "TestCtrl"
        });
        sp.state("authroot.testB", {
            url: "/testB", 
            template: "<h1>Test B {{User|json}}</h1>", 
            controller: "TestCtrl"
        });
        sp.state("other", {
            url: "/other", 
            template: "<h1>Other</h1>", 
        });
    }]);
    app.controller("TestCtrl", ["$scope", "User", function($scope, User) {$scope.User = User;}]);
    app.factory("UserService", ["$q", "$timeout", function($q, $timeout) {
        function GetUser() {
            console.log("Getting User information from server...");
            var d = $q.defer();
            $timeout(function(){
                console.log("Got User info.");
                d.resolve({UserName:"JohnDoe1", OtherData: "asdf"});
            }, 500);
            return d.promise;
        };
        return {
            GetUser: GetUser
        };
    }]);
})(window.angular);
</script>
</head>
<body ng-app="Test">
    <a ui-sref="authroot.testA">Goto A</a>
    <a ui-sref="authroot.testB">Goto B</a>
    <a ui-sref="other">Goto Other</a>
    <div ui-view>Loading...</div>
</body>
</html>
David Ferretti
  • 1,390
  • 1
  • 14
  • 21

2 Answers2

21

The way I find the ui-router exceptional, is in the behaviour you've just described.

Let's think about some entity, e.g. Contact. So it would be nice to have a right side showing us the list of Contacts, the left part the detail. Please check the The basics of using ui-router with AngularJS for quick overvie about the layout. A cite:

ui-router fully embraces the state-machine nature of a routing system. It allows you to define states, and transition your application to those states. The real win is that it allows you to decouple nested states, and do some very complicated layouts in an elegant way.

You need to think about your routing a bit differently, but once you get your head around the state-based approach, I think you will like it.

Ok, why that all?

Because we can have a state Contact representing a List. Say a fixed list from perspective of the detail. (Skip list paging filtering now) We can click on the list and get moved to a state Contact.Detail(ID), to see just selected item. And then select another contact/item.

Here the advantage of nested states comes:

The List (the state Contact) is not reloaded. While the child state Contact.Detail is.

That should explain why the "weird" behaviour should be treated as correct.

To answer your question, how to handle user state. I would use some very top sibling of a route state, with its separated view and controller and some lifecycle arround... triggered in some cycles

Community
  • 1
  • 1
Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
8

Real Short answer is use:

$rootScope.$on("$stateChangeStart")

to listen for any scope changes and do appropriate actions.

Longer answer is, check out the fiddle: http://jsfiddle.net/jasallen/SZGjN/1/

Note that I've used app.run which means i'm resolving the user for every state change. If you want to limit it to state changes while authRoot is in the parentage, put the check for $stateChangeStart in the authRoot controller.

Jason
  • 564
  • 6
  • 12
  • I ended up doing something of a mix between what you and Radim suggested - having the states resolve their own dependencies, and the states that fall outside of the hierarchy registering listeners on the user service object – David Ferretti Dec 13 '13 at 12:31
  • 1
    @David any way we can see what you ended up doing? I'm running into something similar – BlakeH Feb 19 '14 at 18:04
  • 2
    @SonicTheLichen Sure, I'll put a fiddle together today to show you – David Ferretti Feb 20 '14 at 17:06
  • This isn't a full example, but it shows the gist of it: http://jsfiddle.net/M2vQ5/ - every state that needs the user to be logged in needs to be a sub-state of Root.Auth, and also needs to resolve the user dependency. To reduce this repetition, I made this change... – David Ferretti Feb 20 '14 at 19:25
  • 3
    New example: http://jsfiddle.net/2ebZj/1/ now, I can just use ResolveProvider.User to require each state to check the user is logged in. I also have a few other commonly used resolutions in my ResolveProvider – David Ferretti Feb 20 '14 at 19:29
  • Finally, on the server side (using mvc3), the UserService calls "/User" which either returns a small user object with username, name, and a few other items, or returns a HttpUnauthorizedResult, which causes the UserService.GetUser() promise to reject – David Ferretti Feb 20 '14 at 19:32
  • 1
    @SonicTheLichen realized I didn't @ you so here's this comment just in case – David Ferretti Feb 21 '14 at 00:38
  • Thanks @David! I'm going to review this now. – BlakeH Feb 21 '14 at 13:44
  • Thanks for putting those examples together for me @David. This has been very helpful for me. – BlakeH Feb 21 '14 at 16:38