Dynamic Routes with AngularJS

Monday, March 24, 2014

There is a simple rule in AngularJS that trips up many people because they simply aren’t aware of the rule. The rule is that every module has two phases, a configuration phase and a run phase. During the configuration phase you can only use service providers and constants, but during the run phase you only have access to services, and not the service providers.

One scenario where the rule will trip people up is the scenario where an application needs flexible, dynamic routes. Perhaps the routes are tailored to a user’s roles, like giving additional routes to a superuser, but regardless of the specifics you probably need some information from the server to generate the routes. The typical approach to server communication is to use the $http service, so a first attempt might be to write a config function that uses $http and $routeProvider to put together information on the available routes.

app.config(function ($http, $routeProvider) {

    var routes = $http.get("userInfo");
    // ... register routes with $routeProvider
                   
});

The above code will only generate an error.

Error: [$injector:unpr] Unknown provider: $http

Eventually you’ll figure out that a config function only has access to $httpProvider, not $http. Then you might try a run block, which does give you access to $http for server communication, but …

app.run(function ($http, $routeProvider) {

    var routes = $http.get("userInfo");
    // ... register routes with $routeProvider
    
});

… there is no access to providers during a run block.

[$injector:unpr] Unknown provider: $routeProviderProvider

There are a few different approaches to tackling this problem.

One approach would be to use a different wrapper for service communication, like jQuery’s $.get, perhaps combined with manual bootstrapping of the Angular application to ensure you have everything from the server you need to get started.

An Solution With C# and Razor

Another approach would be to use server side rendering to embed the information you need into the shell page of the application. For example, let’s say you are using the following class definitions.

public class ClientRoute
{
    public string Path { get; set; }
    public ClientRouteProperties Properties { get; set; }
}

public class ClientRouteProperties
{
    public string TemplateUrl { get; set; }
    public string Controller { get; set; }
    public string Resolve { get; set; }
}

And also a ClientRouteBuilder that can generate client side routes given the identity of a  user.

public class ClientRouteBuilder
{
    public string BuildRoutesFor(IPrincipal user)
    {
        var routes = new List<ClientRoute>()
        {
            new ClientRoute { 
                Path = "/index",
                Properties = new ClientRouteProperties
                {
                    TemplateUrl = "index.html",
                    Controller = "IndexController"
                }
            }
            
            // ... more routes
        };

        if (user.IsInRole("admin"))
        {
            routes.Add(new ClientRoute
            {
                Path = "/admin",
                Properties = new ClientRouteProperties
                {
                    TemplateUrl = "admin.html",
                    Controller = "AdminController"
                }
            });
        }

        return JsonConvert.SerializeObject(routes,new JsonSerializerSettings()
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }
}

In a Razor view you can use the builder to emit a JavaScript data structure with all the required routes, and embed the JavaScript required to config the application in the view as well.

<body ng-app="app">
    <div ng-view>
        
    </div>
    
    <script src="~/Scripts/angular.js"></script>
    <script src="~/Scripts/angular-route.js"></script>
    <script>
        (function() {

            var app = angular.module("app", ["ngRoute"]);

            /*** embed the routes ***/
            var routes = @Html.Raw(new ClientRouteBuilder().BuildRoutesFor(User))

            /*** register the routes ***/
            app.config(function ($routeProvider) {
                routes.forEach(function(route) {
                    $routeProvider.when(route.path, route.properties);
                 });
                $routeProvider.otherwise({
                    redirectTo: routes[0].path
                });
            });
        }());    

    </script>
    @* Rest of the app scripts *@
</body>

And Remember

You can’t effectively enforce security on the client, so views and API calls still need authorization on the server to make sure a malicious user hasn’t manipulated the routes.


Comments
gravatar Arun Mahendrakar Monday, March 24, 2014
Thanks so much for this Scott.
gravatar Robert Koritnik Tuesday, March 25, 2014
These look nice, but these can hardly be called dynamic routes, because from the ngApp perspective they are still static routes.They're just predefined on the server side and statically provided to client. True dynamic routes should be loaded on demand on the client and resolved there... Because you still have to preload all controllers handling ALL routes regardless of your security settings and route configuration. Some of those controllers may undisclose too much info to those that don't have permission...
gravatar Scott Tuesday, March 25, 2014
@Robert: Sure, the routes aren't changing as the program executes, but they are generated based on the client's identity instead of hard coded. I always have to implement security on the server side to make sure APIs are only giving data to authorized users. If you also want to use the config to avoid loading all controllers there is a little more work to do, one example here: https://github.com/matys84pl/angularjs-requirejs-lazy-controllers
gravatar Mike George Tuesday, March 25, 2014
This is genius and so simple! I've been struggling to implement route security solely in angular and this solution makes it incredibly easy. Thanks!
gravatar webdevelopmentcompanyinchennai Wednesday, March 26, 2014
Wow Really very useful information. Thanks a lot for sharing it with us. I will look forward to read more from you. Could I share this information on my site. http://webdesigningcompanyinchennai.in/”>Web Designing in Chennai.
gravatar Greg Wednesday, March 26, 2014
How about my solution? Or maybe a combination of the two? If you add any HTML files to a specified directory they are automatically accessible via the URL as dynamic routes. https://github.com/gregorypratt/AngularDynamicRouting
gravatar Scott Wednesday, March 26, 2014
@Greg - very cool!
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!